Line data Source code
1 : // SPDX-FileCopyrightText: 2014 Anne Jan Brouwer
2 : // SPDX-License-Identifier: GPL-3.0-or-later
3 : /**
4 : * @file
5 : * @brief Settings dialog: every page of the configuration window and the
6 : * profile table, with the validation that gates OK.
7 : */
8 : #ifndef SRC_CONFIGDIALOG_H_
9 : #define SRC_CONFIGDIALOG_H_
10 :
11 : #include "appsettings.h"
12 : #include "enums.h"
13 : #include "passwordconfiguration.h"
14 : #include "profile.h"
15 :
16 : #include <QDialog>
17 : #include <QHash>
18 : #include <QProcessEnvironment>
19 : #include <QSet>
20 : #include <optional>
21 :
22 : namespace Ui {
23 : struct UserInfo;
24 :
25 : class ConfigDialog;
26 : } // namespace Ui
27 :
28 : class QLabel;
29 :
30 : /**
31 : * @class ConfigDialog
32 : * @ingroup ui
33 : * @brief The ConfigDialog handles the configuration interface.
34 : *
35 : * Presents controls for profiles, generation tools (pwgen/pass),
36 : * GPG/git/OTP/qrencode integration, template handling, tray icon and
37 : * autoclear options, and exposes methods to read and update those settings.
38 : */
39 : class ConfigDialog : public QDialog {
40 454 : Q_OBJECT
41 :
42 : public:
43 : /**
44 : * @brief Construct a ConfigDialog associated with the given main window.
45 : * @param parent Parent widget, usually the main window.
46 : */
47 : explicit ConfigDialog(QWidget *parent);
48 : ~ConfigDialog() override;
49 :
50 : /**
51 : * @brief Whether a signing-key setting holds only full key fingerprints
52 : * (40 or 64 hexadecimal characters each, space separated); empty passes.
53 : * @param setting The profile's signing key field.
54 : * @return true when every token is a fingerprint.
55 : */
56 : static auto isFingerprintList(const QString &setting) -> bool;
57 :
58 : /**
59 : * @brief Enable or disable clipboard selection mode.
60 : * @param useSelection true to enable selection mode.
61 : */
62 : void useSelection(bool useSelection);
63 :
64 : /**
65 : * @brief Return all configured profiles.
66 : * @return Hash of profile name to key-value settings map.
67 : */
68 : auto getProfiles() -> Profiles;
69 :
70 : /**
71 : * @brief Show or hide the system tray icon.
72 : * @param useSystray true to enable the tray icon.
73 : */
74 : void useTrayIcon(bool useSystray);
75 :
76 : /**
77 : * @brief Enable or disable git integration.
78 : * @param useGit true to enable git.
79 : */
80 : void useGit(bool useGit);
81 :
82 : /**
83 : * @brief Enable or disable OTP support.
84 : * @param useOtp true to enable OTP.
85 : */
86 : void useOtp(bool useOtp);
87 :
88 : /**
89 : * @brief Enable or disable grep content search.
90 : * @param useGrepSearch true to enable grep search.
91 : */
92 : void useGrepSearch(bool useGrepSearch);
93 :
94 : /**
95 : * @brief Enable or disable QR code display.
96 : * @param useQrencode true to enable qrencode.
97 : */
98 : void useQrencode(bool useQrencode);
99 :
100 : /**
101 : * @brief Set the path to the pwgen executable.
102 : * @param path Path to pwgen.
103 : */
104 : void setPwgenPath(const QString &path);
105 :
106 : /**
107 : * @brief Enable or disable pwgen-based password generation.
108 : * @param usePwgen true to use pwgen.
109 : */
110 : void usePwgen(bool usePwgen);
111 :
112 : /**
113 : * @brief Apply a password configuration to the dialog controls.
114 : * @param config Configuration to display.
115 : */
116 : void setPasswordConfiguration(const PasswordConfiguration &config);
117 :
118 : /**
119 : * @brief Read the current password configuration from the dialog controls.
120 : * @return Current PasswordConfiguration.
121 : */
122 : auto getPasswordConfiguration() -> PasswordConfiguration;
123 :
124 : protected:
125 : /**
126 : * @brief Forget the signing keys gpg did not know while the dialog is in
127 : * the background, and check them again once it is back in front.
128 : * @param event The change; only activation changes matter here.
129 : */
130 : void changeEvent(QEvent *event) override;
131 :
132 : private slots:
133 : /**
134 : * @brief On OK: warn about a dubious SSH_AUTH_SOCK override, save the
135 : * settings and the profiles, let the active profile follow a rename, and
136 : * create and initialise the stores of profiles added in this session.
137 : */
138 : void on_accepted();
139 : /**
140 : * @brief Fill the pass, gpg (gpg2 first), git and pwgen paths from PATH,
141 : * keeping a field as it is when nothing is found, and choose the pass
142 : * backend when pass was found.
143 : */
144 : void on_autodetectButton_clicked();
145 : /// Native (gpg/git) backend chosen: enable its group box, disable pass's.
146 : void on_radioButtonNative_clicked();
147 : /// pass backend chosen: enable its group box, disable the native one.
148 : void on_radioButtonPass_clicked();
149 : /**
150 : * @brief Pick the git executable in a file dialog; a cancelled dialog
151 : * turns Git off and disables its checkbox, as a missing git does.
152 : */
153 : void on_toolButtonGit_clicked();
154 : /// Pick the gpg executable in a file dialog; cancel keeps the field.
155 : void on_toolButtonGpg_clicked();
156 : /// Open KeygenDialog for the gpg shown in the dialog.
157 : void on_pushButtonGenerateKey_clicked();
158 : /**
159 : * @brief Pick the pwgen executable; a choice enables "use pwgen", a
160 : * cancelled dialog unticks and disables it.
161 : */
162 : void on_toolButtonPwgen_clicked();
163 : /// Pick the pass executable in a file dialog; cancel keeps the field.
164 : void on_toolButtonPass_clicked();
165 : /// Pick the password store folder for the pass backend's store path.
166 : void on_toolButtonStore_clicked();
167 : /**
168 : * @brief Enable the controls that only mean something with a clipboard
169 : * (selection, hiding, autoclear) unless "No clipboard" is chosen.
170 : * @param index The combo box row; 0 is "No clipboard".
171 : */
172 : void on_comboBoxClipboard_activated(int index);
173 : /**
174 : * @brief Show the chosen character set's characters in the line edit,
175 : * editable only for the custom set.
176 : * @param index A PasswordConfiguration::CharacterSet value.
177 : */
178 : void on_passwordCharTemplateSelector_activated(int index);
179 : /// Re-apply the clipboard gating for the current clipboard mode.
180 : void on_checkBoxSelection_clicked();
181 : /**
182 : * @brief Append a "New profile" row pointing at the global store path,
183 : * select it and put the cursor in the name field.
184 : */
185 : void on_addButton_clicked();
186 : /**
187 : * @brief Remove the selected profile row (a message when none is); the
188 : * list's currentRowChanged re-points the form at a neighbour.
189 : */
190 : void on_deleteButton_clicked();
191 : /// Pick the selected profile's store folder and store it in its entry.
192 : void on_profilePathBrowse_clicked();
193 : /// Hide-on-close and start-minimised are only offered with a tray icon.
194 : void on_checkBoxUseTrayIcon_clicked();
195 : /**
196 : * @brief Enable the Git-dependent boxes (add .gpg-id, auto pull/push, and
197 : * in native mode the parent-repository switch) with "use Git".
198 : */
199 : void on_checkBoxUseGit_clicked();
200 : /**
201 : * @brief Enable pwgen's flags or the character-set controls, whichever
202 : * the "use pwgen" box selects; nothing in pass mode, where pass generates.
203 : */
204 : void on_checkBoxUsePwgen_clicked();
205 : /**
206 : * @brief The profile list's current row changed: show that profile.
207 : * @param row The new row, -1 for none.
208 : */
209 : void onProfileSelected(int row);
210 : /**
211 : * @brief The name field was typed in: store it in the entry and the list
212 : * item, then validate (duplicates, empty).
213 : * @param name The text as typed.
214 : */
215 : void onProfileNameEdited(const QString &name);
216 : /**
217 : * @brief The path field was typed in: store it in the entry, validate and
218 : * refresh the status line.
219 : * @param path The text as typed.
220 : */
221 : void onProfilePathEdited(const QString &path);
222 : /**
223 : * @brief The signing-key field was typed in: store it in the entry and
224 : * validate (fingerprints only, shared keys).
225 : * @param key The text as typed.
226 : */
227 : void onProfileSigningKeyEdited(const QString &key);
228 : /**
229 : * @brief One of the profile's Git boxes was clicked: all three flags
230 : * become the profile's own (no longer following the globals), and push
231 : * and pull are enabled only with Git.
232 : */
233 : void onProfileGitToggled();
234 :
235 : private:
236 : /// The secret-fields and every-field checkboxes only while passwords are
237 : /// hidden; the secret words and "Default" only while secret fields are
238 : /// hidden and every field is not.
239 : void updateSecretFieldsEnabled();
240 : /**
241 : * @brief One row of the profile list while the dialog is open. A QList
242 : * rather than the Profiles map so a profile can pass through an empty or
243 : * duplicate name while it is being typed; getProfiles() builds the map.
244 : */
245 : struct ProfileEntry {
246 : QString name; ///< The name as typed; may be empty or a duplicate.
247 : Profile profile; ///< Path, signing key and Git flags as edited.
248 : /// The name the profile had when the dialog opened (empty for one added
249 : /// here), so a rename of the active profile can follow it.
250 : QString originalName;
251 : };
252 : /// What is wrong with one profile's fields; empty strings where nothing is.
253 : struct ProfileProblems {
254 : QString name; ///< Empty or duplicate name; empty when fine.
255 : QString path; ///< Missing store path; empty when fine.
256 : QString key; ///< Signing key that is no fingerprint list; empty when fine.
257 : /// Not a problem, a warning: other profiles sign with the same key.
258 : /// It neither blocks OK nor marks the row.
259 : QString sharedKey;
260 : /**
261 : * @brief Whether all three fields are fine.
262 : * @return true when name, path and key have no complaint.
263 : */
264 : [[nodiscard]] auto none() const -> bool {
265 395 : return name.isEmpty() && path.isEmpty() && key.isEmpty();
266 : }
267 : /**
268 : * @brief The first complaint, for a tooltip on the profile's row.
269 : * @return The name, path or key complaint, in that order; empty if none.
270 : */
271 7 : [[nodiscard]] auto first() const -> QString {
272 7 : return !name.isEmpty() ? name : !path.isEmpty() ? path : key;
273 : }
274 : };
275 : /// Designer tooltip of the profile-name field, put back when validate()
276 : /// has no complaint about it.
277 : QString m_nameTip;
278 : /// Designer tooltip of the store-path field, put back when validate() has
279 : /// no complaint about it.
280 : QString m_pathTip;
281 : /// Designer tooltip of the signing-key field, put back when validate() has
282 : /// no complaint about it.
283 : QString m_keyTip;
284 : /// Below the signing key: the profiles that share it, if any.
285 : QLabel *m_sharedKeyWarning = nullptr;
286 : /// In the pass box: the stores pass would commit outside of, if any.
287 : QLabel *m_passGitWarning = nullptr;
288 : /// Primary fingerprint per gpg and signing-key token (empty: gpg does
289 : /// not know it), so each key is looked up once, not on every keystroke.
290 : mutable QHash<QString, QString> m_primaryOf;
291 : /// An unknown key was forgotten in the background: validate() again on
292 : /// return.
293 : bool m_recheckSigners = false;
294 : /// The settings naming the GPG home those lookups run in, read on first
295 : /// use.
296 : mutable std::optional<AppSettings> m_gpgSettings;
297 : /// The profiles as shown; index == row in profileList.
298 : QList<ProfileEntry> m_entries;
299 : /// Row whose fields the form shows, -1 for none.
300 : int m_currentEntry = -1;
301 : /// True while loadProfileForm() sets the fields, so their edited-signals
302 : /// do not write back.
303 : bool m_loadingForm = false;
304 :
305 : /**
306 : * @brief Show row @p row's profile in the form (or clear and disable it
307 : * for -1), with m_loadingForm set so the fields' edited-signals do not
308 : * write back. The Git boxes show the profile's own flags, else the
309 : * global ones.
310 : * @param row Row in m_entries, or -1 for none.
311 : */
312 : void loadProfileForm(int row);
313 : /**
314 : * @brief The entry the form is editing.
315 : * @return A pointer into m_entries, or nullptr when no row is current.
316 : */
317 : auto currentEntry() -> ProfileEntry *;
318 : /**
319 : * @brief Refresh the status line under the form: the profile's name and
320 : * cleaned path, marked "New" while the folder does not exist, or a
321 : * reminder to fill in the required fields.
322 : */
323 : void updateProfileStatus();
324 : /// The widgets from configdialog.ui.
325 : QScopedPointer<Ui::ConfigDialog> ui;
326 :
327 : /**
328 : * @brief Show @p path as the git executable; an empty path disables and
329 : * turns off "use Git".
330 : * @param path The git executable, or empty for none.
331 : */
332 : void setGitPath(const QString &path);
333 : /**
334 : * @brief Fill the profile list and m_entries from @p profiles, dropping
335 : * the empty-named leftover older versions wrote, select
336 : * @p currentProfile and validate.
337 : * @param profiles The saved profiles.
338 : * @param currentProfile The active profile's name, to select its row.
339 : */
340 : void setProfiles(Profiles profiles, const QString ¤tProfile);
341 : /**
342 : * @brief Tick the pass or the native radio button and enable the matching
343 : * group box.
344 : * @param usePass true for the pass backend.
345 : */
346 : void usePass(bool usePass);
347 :
348 : /**
349 : * @brief Load the dialog's value widgets from an AppSettings struct.
350 : *
351 : * Mirrors the legacy per-getter constructor loads. Sets values only; the
352 : * constructor still handles environment-dependent gating (tray/OTP/qrencode
353 : * availability, clipboard combo population, primary-selection support) and
354 : * the profile table. Password-generation and pwgen-path widgets are
355 : * intentionally not loaded here, matching the previous behaviour.
356 : * @param settings Values to display.
357 : */
358 : void applySettings(const AppSettings &settings);
359 : /**
360 : * @brief Build an AppSettings struct from the dialog's widgets.
361 : *
362 : * Starts from the currently persisted settings and overwrites only the
363 : * fields this dialog owns, so unrelated keys (window geometry,
364 : * profiles, etc.) are preserved on save.
365 : * @return Settings reflecting the dialog state.
366 : */
367 : auto readSettings() -> AppSettings;
368 :
369 : /**
370 : * @brief Enable the group box of the chosen backend and, in pass mode,
371 : * disable the password-generation controls pass takes care of; in native
372 : * mode hand them back to on_checkBoxUsePwgen_clicked().
373 : */
374 : void setGroupBoxState();
375 : /**
376 : * @brief Ask for an existing file in a read-only file dialog.
377 : * @return The chosen file, empty when cancelled.
378 : */
379 : auto selectExecutable() -> QString;
380 : /**
381 : * @brief Ask for a directory in a directories-only file dialog.
382 : * @return The chosen folder, empty when cancelled.
383 : */
384 : auto selectFolder() -> QString;
385 : /**
386 : * @brief QMessageBox::critical with a single OK button. Once a workaround
387 : * for crashes in Qt 5.4.1 before QApplication::exec(); no caller today.
388 : * @param title The box's title.
389 : * @param text The message.
390 : */
391 : void criticalMessage(const QString &title, const QString &text);
392 :
393 : /**
394 : * @brief Whether qrencode can run: @p configuredPath when it is an
395 : * executable file, else a `qrencode` on Util's PATH, which is then saved
396 : * as the qrencode executable. Always false on Windows.
397 : * @param configuredPath The saved qrencode executable, possibly empty.
398 : * @return true when QR codes can be shown.
399 : */
400 : auto isQrencodeAvailable(const QString &configuredPath) -> bool;
401 : /**
402 : * @brief Check every profile (problemsOf(), sharingTheKey()), mark the
403 : * rows and the form, enable OK only when no profile has a problem, and
404 : * refresh the pass Git warning.
405 : */
406 : void validate();
407 : /// Shows m_passGitWarning for the stores Util::repositoryPassClimbsTo()
408 : /// names, while the pass backend is chosen.
409 : void updatePassGitWarning();
410 : /**
411 : * @brief What is wrong with @p entry's name, store path and signing keys.
412 : * @param entry The profile to check.
413 : * @param names How many profiles carry each name, for duplicates.
414 : * @return The complaints, empty where a field is fine.
415 : */
416 : auto problemsOf(const ProfileEntry &entry,
417 : const QHash<QString, int> &names) const -> ProfileProblems;
418 : /**
419 : * @brief Mark row @p row of the profile list, and the form fields when it
420 : * is the row being edited, with @p problems.
421 : * @param row Row in the profile list.
422 : * @param problems What problemsOf() found for it.
423 : */
424 : void showProblems(int row, const ProfileProblems &problems);
425 : /// Per profile row: its store as the file system resolves it, and its
426 : /// signing-key fingerprints (upper case).
427 201 : struct SigningStores {
428 : QStringList stores; ///< Canonical (or cleaned) store path.
429 : QList<QSet<QString>> keys; ///< Fingerprints of the signing key.
430 : };
431 : /**
432 : * @brief Resolve every profile's store and fingerprints once.
433 : * @return One entry per row of m_entries.
434 : */
435 : [[nodiscard]] auto signingStores() const -> SigningStores;
436 : /**
437 : * @brief One profile's signing keys as signers: each key as written, plus
438 : * the primary fingerprint gpg gives for a complete key ID or (sub)key
439 : * fingerprint.
440 : * @param signingKey The profile's signing-key setting.
441 : * @param env The environment gpg looks keys up in (its GPG home).
442 : * @return Upper-case fingerprints and key IDs.
443 : */
444 : [[nodiscard]] auto signerKeys(const QString &signingKey,
445 : const QProcessEnvironment &env) const
446 : -> QSet<QString>;
447 : /**
448 : * @brief The other profiles, for other stores, whose signing keys share a
449 : * fingerprint with row @p row's.
450 : * @param row The profile's row.
451 : * @param resolved What signingStores() gave for this validation.
452 : * @return Their names, sorted; empty when none or no key is set.
453 : */
454 : [[nodiscard]] auto sharingTheKey(int row, const SigningStores &resolved) const
455 : -> QStringList;
456 :
457 : /**
458 : * @brief For every saved profile whose store was not a profile's store
459 : * when the dialog opened: offer to create the folder and, when it has no
460 : * `.gpg-id`, ask for recipients and initialise it.
461 : * @param existingProfiles The profiles as they were when the dialog opened.
462 : */
463 : void initializeNewProfiles(const Profiles &existingProfiles);
464 : /**
465 : * @brief Offer to create a new profile's store folder when it is missing.
466 : * @param path The folder, cleaned.
467 : * @return Whether the folder exists now.
468 : */
469 : auto ensureProfileFolder(const QString &path) -> bool;
470 : /**
471 : * @brief Ask for a new profile's recipients and initialise its store with
472 : * them, keeping the active backend's settings and queue on the active
473 : * store (#1774).
474 : * @param name The profile's name, for titles and messages.
475 : * @param profile The profile.
476 : * @param path Its store folder, cleaned.
477 : */
478 : void initialiseProfileStore(const QString &name, const Profile &profile,
479 : const QString &path);
480 : /// Warn, without refusing, when the SSH_AUTH_SOCK override does not look
481 : /// like a socket; the value is saved as entered either way.
482 : void warnAboutSshAuthSockOverride();
483 : /**
484 : * @brief Point the active profile at its new name after a rename (or at
485 : * none after a deletion), and let its own Git flags replace the global
486 : * ones, as switching to it does.
487 : * @param profiles The profiles as saved.
488 : */
489 : void followActiveProfile(const Profiles &profiles);
490 :
491 : /// User-defined custom charset, retained while a builtin set is selected so
492 : /// it is not lost when the line edit shows the builtin's characters instead.
493 : QString m_customPasswordChars;
494 : };
495 :
496 : #endif // SRC_CONFIGDIALOG_H_
|