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 Implements ConfigDialog: loading and reading the widgets
6 : * (applySettings(), readSettings()), the profile table with its validation
7 : * (validate(), problemsOf(), sharingTheKey()) and the initialisation of
8 : * stores for profiles added in the dialog (initializeNewProfiles()).
9 : */
10 : #include "configdialog.h"
11 : #include "appsettings.h"
12 : #include "executor.h"
13 : #include "gpgkeystate.h"
14 : #include "keygendialog.h"
15 : #include "profileinit.h"
16 : #include "qtpasssettings.h"
17 : #include "sshauthsock.h"
18 : #include "ui_configdialog.h"
19 : #include "usersdialog.h"
20 : #include "util.h"
21 : #include "windowstatestore.h"
22 : #include <QClipboard>
23 : #include <QDir>
24 : #include <QEvent>
25 : #include <QFileDialog>
26 : #include <QFileInfo>
27 : #include <QFormLayout>
28 : #include <QGuiApplication>
29 : #include <QHash>
30 : #include <QLabel>
31 : #include <QListWidgetItem>
32 : #include <QMessageBox>
33 : #include <QPushButton>
34 : #include <QRegularExpression>
35 : #include <QSet>
36 : #include <QSystemTrayIcon>
37 : #include <algorithm>
38 : #include <utility>
39 : #ifdef Q_OS_WIN
40 : #include <windows.h>
41 : #endif
42 :
43 : #include "qtpasslogging.h"
44 :
45 72 : ConfigDialog::ConfigDialog(QWidget *parent)
46 72 : : QDialog(parent), ui(new Ui::ConfigDialog) {
47 72 : ui->setupUi(this);
48 :
49 144 : WindowStateStore::attach(*this, QStringLiteral("configDialog"));
50 :
51 : // setSpecialValueText() in retranslateUi() drops the size hint without
52 : // telling the layout, which then sizes the box for "0" and shows "ver".
53 72 : ui->spinBoxAutoclearSeconds->updateGeometry();
54 72 : ui->spinBoxAutoclearPanelSeconds->updateGeometry();
55 :
56 : // The secret field names only mean something while passwords are hidden.
57 72 : connect(ui->checkBoxHidePassword, &QCheckBox::toggled, this,
58 98 : [this] { updateSecretFieldsEnabled(); });
59 72 : connect(ui->checkBoxHideSecretFields, &QCheckBox::toggled, this,
60 147 : [this] { updateSecretFieldsEnabled(); });
61 72 : connect(ui->checkBoxHideAllFields, &QCheckBox::toggled, this,
62 75 : [this] { updateSecretFieldsEnabled(); });
63 72 : connect(ui->buttonDefaultSecretFields, &QPushButton::clicked, this, [this] {
64 2 : ui->lineEditSecretFields->setText(
65 2 : AppSettings::defaultSecretFieldNames().join(QStringLiteral(", ")));
66 1 : });
67 : // "Seconds" says nothing next to "Never" (the spin box's 0).
68 : // Restoring is part of clearing: only there when the clipboard is cleared.
69 72 : connect(ui->spinBoxAutoclearSeconds, &QSpinBox::valueChanged, this,
70 72 : [this](int value) {
71 29 : ui->labelSeconds->setVisible(value > 0);
72 29 : ui->checkBoxRestoreClipboard->setVisible(value > 0);
73 29 : });
74 72 : connect(ui->spinBoxAutoclearPanelSeconds, &QSpinBox::valueChanged, this,
75 102 : [this](int value) { ui->labelPanelSeconds->setVisible(value > 0); });
76 72 : connect(
77 72 : ui->comboBoxFields, &QComboBox::currentIndexChanged, this,
78 105 : [this](int index) { ui->plainTextEditTemplate->setEnabled(index > 0); });
79 :
80 72 : const AppSettings s = QtPassSettings::load();
81 72 : applySettings(s);
82 :
83 72 : if (!QSystemTrayIcon::isSystemTrayAvailable()) {
84 72 : ui->checkBoxUseTrayIcon->setEnabled(false);
85 144 : ui->checkBoxUseTrayIcon->setToolTip(tr("System tray is not available"));
86 72 : ui->checkBoxHideOnClose->setEnabled(false);
87 72 : ui->checkBoxStartMinimized->setEnabled(false);
88 : }
89 :
90 : // Wayland has no client-side "keep above" — Qt::WindowStaysOnTopHint is
91 : // silently ignored there (stacking belongs to the compositor). Say so
92 : // instead of offering a checkbox that does nothing.
93 72 : if (QGuiApplication::platformName().startsWith(QLatin1String("wayland"))) {
94 0 : ui->checkBoxAlwaysOnTop->setEnabled(false);
95 0 : ui->checkBoxAlwaysOnTop->setToolTip(
96 0 : tr("Not available on Wayland; use your compositor's "
97 : "\"keep above\" window rule instead"));
98 : }
99 :
100 : #if defined(Q_OS_WIN)
101 : // checkBoxUseOtp stays visible: OTP is generated in-process, not by the
102 : // Unix-only pass-otp extension.
103 : ui->checkBoxUseQrencode->hide();
104 : #endif
105 :
106 72 : if (!isQrencodeAvailable(s.qrencodeExecutable)) {
107 70 : ui->checkBoxUseQrencode->setEnabled(false);
108 140 : ui->checkBoxUseQrencode->setToolTip(tr("qrencode needs to be installed"));
109 : }
110 :
111 72 : connect(ui->profileList, &QListWidget::currentRowChanged, this,
112 72 : &ConfigDialog::onProfileSelected);
113 72 : connect(ui->profileName, &QLineEdit::textEdited, this,
114 72 : &ConfigDialog::onProfileNameEdited);
115 72 : connect(ui->profilePath, &QLineEdit::textEdited, this,
116 72 : &ConfigDialog::onProfilePathEdited);
117 72 : connect(ui->profileSigningKey, &QLineEdit::textEdited, this,
118 144 : &ConfigDialog::onProfileSigningKeyEdited);
119 504 : for (QCheckBox *box :
120 288 : {ui->profileUseGit, ui->profileAutoPush, ui->profileAutoPull}) {
121 216 : connect(box, &QCheckBox::clicked, this, &ConfigDialog::onProfileGitToggled);
122 : }
123 144 : m_nameTip = ui->profileName->toolTip();
124 144 : m_pathTip = ui->profilePath->toolTip();
125 72 : m_keyTip = ui->profileSigningKey->toolTip();
126 : // A warning, not a problem: shown under the signing key while another
127 : // profile, for another store, signs with the same key (#2050).
128 72 : m_sharedKeyWarning = new QLabel(this);
129 144 : m_sharedKeyWarning->setObjectName(QStringLiteral("profileSharedKeyWarning"));
130 72 : m_sharedKeyWarning->setTextFormat(Qt::PlainText);
131 72 : m_sharedKeyWarning->setWordWrap(true);
132 72 : m_sharedKeyWarning->hide();
133 : {
134 72 : int row = -1;
135 72 : QFormLayout::ItemRole role = QFormLayout::FieldRole;
136 72 : ui->profileFormLayout->getWidgetPosition(ui->profileSigningKey, &row,
137 : &role);
138 72 : ui->profileFormLayout->insertRow(row + 1, QString(), m_sharedKeyWarning);
139 : }
140 : // Under the pass settings: a store pass would commit outside of.
141 72 : m_passGitWarning = new QLabel(this);
142 144 : m_passGitWarning->setObjectName(QStringLiteral("passGitWarning"));
143 72 : m_passGitWarning->setTextFormat(Qt::PlainText);
144 72 : m_passGitWarning->setWordWrap(true);
145 72 : m_passGitWarning->hide();
146 72 : ui->groupBoxPass->layout()->addWidget(m_passGitWarning);
147 72 : connect(ui->storePath, &QLineEdit::textChanged, this,
148 154 : [this] { updatePassGitWarning(); });
149 216 : setProfiles(QtPassSettings::getProfiles(), QtPassSettings::getProfile());
150 :
151 144 : ui->label->setText(ui->label->text() + VERSION);
152 72 : ui->comboBoxClipboard->clear();
153 :
154 144 : ui->comboBoxClipboard->addItem(tr("No clipboard"));
155 144 : ui->comboBoxClipboard->addItem(tr("Always copy to clipboard"));
156 144 : ui->comboBoxClipboard->addItem(tr("On-demand copy to clipboard"));
157 :
158 72 : int currentIndex = static_cast<int>(s.clipBoardType);
159 72 : ui->comboBoxClipboard->setCurrentIndex(currentIndex);
160 72 : on_comboBoxClipboard_activated(currentIndex);
161 :
162 72 : QClipboard *clip = QApplication::clipboard();
163 72 : if (!clip->supportsSelection()) {
164 72 : useSelection(false);
165 72 : ui->checkBoxSelection->setVisible(false);
166 : } else {
167 0 : useSelection(s.useSelection);
168 : }
169 :
170 72 : connect(ui->pageList, &QListWidget::currentRowChanged, ui->pages,
171 144 : &QStackedWidget::setCurrentIndex);
172 142 : ui->pageList->setCurrentRow(
173 72 : Util::configIsValid(s)
174 : ? 0
175 : // Programs is what needs fixing when the configuration is not valid.
176 70 : : ui->pages->indexOf(ui->pagePrograms));
177 :
178 72 : connect(this, &ConfigDialog::accepted, this, &ConfigDialog::on_accepted);
179 72 : }
180 :
181 72 : void ConfigDialog::applySettings(const AppSettings &settings) {
182 72 : ui->passPath->setText(settings.passExecutable);
183 72 : setGitPath(settings.gitExecutable);
184 72 : ui->gpgPath->setText(settings.gpgExecutable);
185 72 : ui->storePath->setText(settings.passStore);
186 72 : ui->sshAuthSockOverride->setText(settings.sshAuthSockOverride);
187 :
188 : // One control each: 0 is "Never", so off and a delay are one setting.
189 72 : ui->spinBoxAutoclearSeconds->setValue(
190 72 : settings.useAutoclear ? settings.autoclearSeconds : 0);
191 72 : ui->labelSeconds->setVisible(ui->spinBoxAutoclearSeconds->value() > 0);
192 72 : ui->checkBoxRestoreClipboard->setChecked(settings.restoreClipboard);
193 72 : ui->checkBoxRestoreClipboard->setVisible(
194 72 : ui->spinBoxAutoclearSeconds->value() > 0);
195 72 : ui->spinBoxAutoclearPanelSeconds->setValue(
196 72 : settings.useAutoclearPanel ? settings.autoclearPanelSeconds : 0);
197 72 : ui->labelPanelSeconds->setVisible(ui->spinBoxAutoclearPanelSeconds->value() >
198 : 0);
199 72 : ui->checkBoxHidePassword->setChecked(settings.hidePassword);
200 72 : ui->checkBoxHideSecretFields->setChecked(settings.hideSecretFields);
201 72 : ui->checkBoxHideAllFields->setChecked(settings.hideAllFields);
202 72 : ui->lineEditSecretFields->setText(
203 144 : settings.secretFieldNames.join(QStringLiteral(", ")));
204 72 : updateSecretFieldsEnabled();
205 72 : ui->checkBoxHideContent->setChecked(settings.hideContent);
206 72 : ui->checkBoxUseMonospace->setChecked(settings.useMonospace);
207 72 : ui->checkBoxDisplayAsIs->setChecked(settings.displayAsIs);
208 72 : ui->checkBoxNoLineWrapping->setChecked(settings.noLineWrapping);
209 72 : ui->checkBoxAddGPGId->setChecked(settings.addGPGId);
210 72 : ui->checkBoxHideOnClose->setChecked(settings.hideOnClose);
211 72 : ui->checkBoxStartMinimized->setChecked(settings.startMinimized);
212 :
213 72 : ui->checkBoxAvoidCapitals->setChecked(settings.avoidCapitals);
214 72 : ui->checkBoxAvoidNumbers->setChecked(settings.avoidNumbers);
215 72 : ui->checkBoxLessRandom->setChecked(settings.lessRandom);
216 72 : ui->checkBoxUseSymbols->setChecked(settings.useSymbols);
217 72 : ui->plainTextEditTemplate->setPlainText(settings.passTemplate);
218 : // All fields only applies while templating is on (#1766), so the two
219 : // switches are one choice of three.
220 72 : ui->comboBoxFields->setCurrentIndex(
221 72 : !settings.useTemplate ? 0 : (settings.templateAllFields ? 2 : 1));
222 72 : ui->plainTextEditTemplate->setEnabled(settings.useTemplate);
223 72 : ui->checkBoxAutoPull->setChecked(settings.autoPull);
224 72 : ui->checkBoxAutoPush->setChecked(settings.autoPush);
225 72 : ui->checkBoxGitUseParentRepository->setChecked(
226 72 : settings.gitUseParentRepository);
227 72 : ui->checkBoxAlwaysOnTop->setChecked(settings.alwaysOnTop);
228 :
229 : // Dependent helpers: set after the plain values above so the enable/disable
230 : // logic they trigger sees the final widget states.
231 72 : usePass(settings.usePass);
232 72 : useTrayIcon(settings.useTrayIcon);
233 72 : useGit(settings.useGit);
234 72 : useOtp(settings.useOtp);
235 72 : useGrepSearch(settings.useGrepSearch);
236 72 : useQrencode(settings.useQrencode);
237 : // Before usePwgen (gated on the pwgen path); otherwise readSettings() writes
238 : // the .ui defaults over the saved configuration on every OK.
239 72 : setPwgenPath(settings.pwgenExecutable);
240 72 : setPasswordConfiguration(settings.passwordConfiguration);
241 72 : usePwgen(settings.usePwgen);
242 72 : }
243 :
244 23 : auto ConfigDialog::readSettings() -> AppSettings {
245 : // Start from the persisted settings so keys this dialog does not own
246 : // (window geometry, profiles, etc.) survive the save.
247 23 : AppSettings settings = QtPassSettings::load();
248 :
249 46 : settings.passExecutable = ui->passPath->text();
250 46 : settings.gitExecutable = ui->gitPath->text();
251 46 : settings.gpgExecutable = ui->gpgPath->text();
252 46 : settings.sshAuthSockOverride = ui->sshAuthSockOverride->text().trimmed();
253 46 : settings.passStore = Util::normalizeFolderPath(ui->storePath->text());
254 23 : settings.usePass = ui->radioButtonPass->isChecked();
255 23 : settings.clipBoardType =
256 23 : static_cast<Enums::ClipBoardType>(ui->comboBoxClipboard->currentIndex());
257 23 : settings.useSelection = ui->checkBoxSelection->isChecked();
258 23 : settings.autoclearSeconds = ui->spinBoxAutoclearSeconds->value();
259 23 : settings.useAutoclear = settings.autoclearSeconds > 0;
260 23 : settings.restoreClipboard = ui->checkBoxRestoreClipboard->isChecked();
261 23 : settings.autoclearPanelSeconds = ui->spinBoxAutoclearPanelSeconds->value();
262 23 : settings.useAutoclearPanel = settings.autoclearPanelSeconds > 0;
263 23 : settings.hidePassword = ui->checkBoxHidePassword->isChecked();
264 23 : settings.hideSecretFields = ui->checkBoxHideSecretFields->isChecked();
265 23 : settings.hideAllFields = ui->checkBoxHideAllFields->isChecked();
266 : // As written, an empty list included ("Default" puts the set back).
267 23 : settings.secretFieldNames.clear();
268 : for (const QString &name :
269 214 : ui->lineEditSecretFields->text().split(QLatin1Char(','))) {
270 168 : if (!name.trimmed().isEmpty()) {
271 156 : settings.secretFieldNames << name.trimmed();
272 : }
273 : }
274 23 : settings.hideContent = ui->checkBoxHideContent->isChecked();
275 23 : settings.useMonospace = ui->checkBoxUseMonospace->isChecked();
276 23 : settings.displayAsIs = ui->checkBoxDisplayAsIs->isChecked();
277 23 : settings.noLineWrapping = ui->checkBoxNoLineWrapping->isChecked();
278 23 : settings.addGPGId = ui->checkBoxAddGPGId->isChecked();
279 : // A disabled control (e.g. no system tray) keeps the persisted value
280 : // instead of clobbering it to false.
281 23 : if (ui->checkBoxUseTrayIcon->isEnabled()) {
282 0 : settings.useTrayIcon = ui->checkBoxUseTrayIcon->isChecked();
283 : }
284 23 : if (ui->checkBoxHideOnClose->isEnabled()) {
285 0 : settings.hideOnClose = ui->checkBoxHideOnClose->isChecked();
286 : }
287 23 : if (ui->checkBoxStartMinimized->isEnabled()) {
288 0 : settings.startMinimized = ui->checkBoxStartMinimized->isChecked();
289 : }
290 23 : settings.useGit = ui->checkBoxUseGit->isChecked();
291 : // The backends and the auto-pull-on-start read the global keys; #1140
292 : // started storing these per profile as well and stopped writing the
293 : // globals, which left both checkboxes without effect.
294 23 : settings.autoPush = ui->checkBoxAutoPush->isChecked();
295 23 : settings.autoPull = ui->checkBoxAutoPull->isChecked();
296 23 : settings.gitUseParentRepository =
297 23 : ui->checkBoxGitUseParentRepository->isChecked();
298 23 : settings.useOtp = ui->checkBoxUseOtp->isChecked();
299 23 : settings.useGrepSearch = ui->checkBoxUseGrepSearch->isChecked();
300 23 : settings.useQrencode = ui->checkBoxUseQrencode->isChecked();
301 46 : settings.pwgenExecutable = ui->pwgenPath->text();
302 23 : settings.usePwgen = ui->checkBoxUsePwgen->isChecked();
303 23 : settings.avoidCapitals = ui->checkBoxAvoidCapitals->isChecked();
304 23 : settings.avoidNumbers = ui->checkBoxAvoidNumbers->isChecked();
305 23 : settings.lessRandom = ui->checkBoxLessRandom->isChecked();
306 23 : settings.useSymbols = ui->checkBoxUseSymbols->isChecked();
307 46 : settings.passwordConfiguration = getPasswordConfiguration();
308 23 : settings.useTemplate = ui->comboBoxFields->currentIndex() > 0;
309 46 : settings.passTemplate = ui->plainTextEditTemplate->toPlainText();
310 23 : settings.templateAllFields = ui->comboBoxFields->currentIndex() == 2;
311 23 : settings.alwaysOnTop = ui->checkBoxAlwaysOnTop->isChecked();
312 23 : settings.version = VERSION;
313 :
314 23 : return settings;
315 0 : }
316 :
317 222 : ConfigDialog::~ConfigDialog() = default;
318 :
319 72 : void ConfigDialog::setGitPath(const QString &path) {
320 72 : ui->gitPath->setText(path);
321 72 : ui->checkBoxUseGit->setEnabled(!path.isEmpty());
322 72 : if (path.isEmpty()) {
323 45 : useGit(false);
324 : }
325 72 : }
326 :
327 73 : void ConfigDialog::usePass(bool usePass) {
328 73 : ui->radioButtonNative->setChecked(!usePass);
329 73 : ui->radioButtonPass->setChecked(usePass);
330 73 : setGroupBoxState();
331 73 : }
332 :
333 135 : auto ConfigDialog::isFingerprintList(const QString &setting) -> bool {
334 : // 40 hex characters for OpenPGP v4 keys, 64 for v5/v6 (gpg 2.5+).
335 : static const QRegularExpression fingerprint(
336 137 : QStringLiteral("^(?:[0-9A-Fa-f]{40}|[0-9A-Fa-f]{64})$"));
337 135 : const QStringList keys = setting.split(QLatin1Char(' '), Qt::SkipEmptyParts);
338 135 : return std::all_of(keys.cbegin(), keys.cend(), [](const QString &key) {
339 43 : return fingerprint.match(key).hasMatch();
340 135 : });
341 : }
342 :
343 : // Profile names must be unique: the settings key profiles by name, so a
344 : // duplicate would silently overwrite the other.
345 132 : auto ConfigDialog::problemsOf(const ProfileEntry &entry,
346 : const QHash<QString, int> &names) const
347 : -> ProfileProblems {
348 132 : ProfileProblems problems;
349 132 : if (entry.name.isEmpty()) {
350 1 : problems.name = tr("This field is required");
351 131 : } else if (names.value(entry.name) > 1) {
352 2 : problems.name = tr("Another profile already has this name");
353 : }
354 132 : if (entry.profile.path.isEmpty()) {
355 3 : problems.path = tr("This field is required");
356 : }
357 : // pass compares PASSWORD_STORE_SIGNING_KEY against the fingerprints in
358 : // gpg's VALIDSIG line, so anything shorter than a fingerprint would sign
359 : // and then never verify. Refuse it here instead of failing at first use.
360 132 : if (!isFingerprintList(entry.profile.signingKey)) {
361 1 : problems.key = tr("Full key fingerprints only (40 or 64 hexadecimal "
362 : "characters), separated by spaces");
363 : }
364 132 : return problems;
365 0 : }
366 :
367 132 : void ConfigDialog::showProblems(int row, const ProfileProblems &problems) {
368 132 : if (QListWidgetItem *item = ui->profileList->item(row)) {
369 132 : item->setBackground(problems.none() ? QBrush() : QBrush(Qt::red));
370 264 : item->setToolTip(problems.none() ? problems.sharedKey : problems.first());
371 : }
372 132 : if (row != m_currentEntry) {
373 : return;
374 : }
375 75 : m_sharedKeyWarning->setText(problems.sharedKey);
376 75 : m_sharedKeyWarning->setVisible(!problems.sharedKey.isEmpty());
377 : const auto tipOr = [](const QString &problem, const QString &tip) {
378 225 : return problem.isEmpty() ? tip : problem;
379 : };
380 225 : ui->profileName->setToolTip(tipOr(problems.name, m_nameTip));
381 225 : ui->profilePath->setToolTip(tipOr(problems.path, m_pathTip));
382 225 : ui->profileSigningKey->setToolTip(tipOr(problems.key, m_keyTip));
383 : }
384 :
385 201 : auto ConfigDialog::signingStores() const -> SigningStores {
386 : // One folder spelled two ways (through a link, another case on a
387 : // case-insensitive file system) is one store: compared as the file system
388 : // resolves it where it exists. Resolved once per validate(), not once per
389 : // pair of profiles.
390 201 : SigningStores resolved;
391 : // Keys are looked up in the keyring that signs: the configured GPG home,
392 : // as profiles sign with it, not whatever GNUPGHOME QtPass inherited. The
393 : // dialog does not edit the home, so the settings are read once, not on
394 : // every keystroke; the environment is built each time, as the home may
395 : // only appear meanwhile (a key generated from this dialog).
396 201 : if (!m_gpgSettings) {
397 72 : m_gpgSettings = QtPassSettings::load();
398 : }
399 201 : const QProcessEnvironment env = ProfileInit::gpgEnvironment(*m_gpgSettings);
400 333 : for (const ProfileEntry &entry : std::as_const(m_entries)) {
401 132 : const QString real = QFileInfo(entry.profile.path).canonicalFilePath();
402 132 : resolved.stores << (real.isEmpty() ? QDir::cleanPath(entry.profile.path)
403 132 : : real);
404 264 : resolved.keys << signerKeys(entry.profile.signingKey, env);
405 : }
406 201 : return resolved;
407 201 : }
408 :
409 132 : auto ConfigDialog::signerKeys(const QString &signingKey,
410 : const QProcessEnvironment &env) const
411 : -> QSet<QString> {
412 : // Verification takes the primary or the signing subkey's fingerprint
413 : // from VALIDSIG, and gpg signs for a primary key with its subkey: a key
414 : // written as the subkey in one profile and as the primary in another is
415 : // one signer (#2087). Only complete IDs are asked about, so typing a key
416 : // does not start gpg for every prefix.
417 1 : static const QRegularExpression complete(QStringLiteral(
418 134 : "^(?:0X)?(?:[0-9A-F]{8}|[0-9A-F]{16}|[0-9A-F]{40}|[0-9A-F]{64})$"));
419 132 : const QString gpg = ui->gpgPath->text();
420 : QSet<QString> keys;
421 : for (const QString &key :
422 173 : signingKey.toUpper().split(QLatin1Char(' '), Qt::SkipEmptyParts)) {
423 41 : keys.insert(key);
424 56 : if (gpg.isEmpty() || !complete.match(key).hasMatch()) {
425 26 : continue;
426 : }
427 30 : const QString cacheKey = gpg + QLatin1Char('\n') +
428 30 : env.value(QStringLiteral("GNUPGHOME")) +
429 30 : QLatin1Char('\n') + key;
430 15 : auto cached = m_primaryOf.constFind(cacheKey);
431 : if (cached == m_primaryOf.cend()) {
432 9 : QString out;
433 : // Blocking, once per key: no trustdb check, which a large keyring
434 : // would make slow.
435 99 : const int rc = Executor::executeBlocking(
436 : env, gpg,
437 9 : {QStringLiteral("--no-tty"), QStringLiteral("--batch"),
438 9 : QStringLiteral("--no-auto-check-trustdb"),
439 9 : QStringLiteral("--with-colons"),
440 9 : QStringLiteral("--with-fingerprint"),
441 9 : QStringLiteral("--with-subkey-fingerprint"),
442 9 : QStringLiteral("--list-keys"), QStringLiteral("--"), key},
443 : &out);
444 9 : cached = m_primaryOf.insert(
445 18 : cacheKey, rc == 0 ? primaryFingerprintOf(out, key) : QString());
446 : }
447 15 : if (!cached->isEmpty()) {
448 5 : keys.insert(*cached);
449 : }
450 : }
451 132 : return keys;
452 : }
453 :
454 81 : void ConfigDialog::changeEvent(QEvent *event) {
455 81 : QDialog::changeEvent(event);
456 81 : if (event->type() != QEvent::ActivationChange) {
457 : return;
458 : }
459 : // A key is imported somewhere else: a terminal, a key manager, or the
460 : // key generator this dialog opens. Whatever keyring gpg uses, the dialog
461 : // is in the background meanwhile, so a key gpg did not know is asked
462 : // about again on return.
463 9 : if (!isActiveWindow()) {
464 3 : const auto forgotten = m_primaryOf.removeIf(
465 : [](const std::pair<const QString &, QString &> &lookup) {
466 : return lookup.second.isEmpty();
467 : });
468 4 : m_recheckSigners = m_recheckSigners || forgotten > 0;
469 6 : } else if (m_recheckSigners) {
470 1 : m_recheckSigners = false;
471 1 : validate();
472 : }
473 : }
474 :
475 132 : auto ConfigDialog::sharingTheKey(int row, const SigningStores &resolved) const
476 : -> QStringList {
477 : // A list signed for one store verifies in the other when the stores share
478 : // a signing key; the folder line names a folder, not a store (#2050).
479 132 : QStringList others;
480 : const QSet<QString> &mine = resolved.keys.at(row);
481 : if (mine.isEmpty()) {
482 : return others;
483 : }
484 132 : for (int other = 0; other < m_entries.size(); ++other) {
485 92 : if (other == row || resolved.stores.at(other) == resolved.stores.at(row)) {
486 54 : continue;
487 : }
488 38 : if (resolved.keys.at(other).intersects(mine)) {
489 16 : others << m_entries.at(other).name;
490 : }
491 : }
492 : others.sort();
493 : return others;
494 : }
495 :
496 201 : void ConfigDialog::validate() {
497 201 : QHash<QString, int> names;
498 333 : for (const ProfileEntry &entry : std::as_const(m_entries)) {
499 132 : names[entry.name]++;
500 : }
501 : bool status = true;
502 201 : const SigningStores resolved = signingStores();
503 333 : for (int row = 0; row < m_entries.size(); ++row) {
504 132 : ProfileProblems problems = problemsOf(m_entries.at(row), names);
505 132 : if (const QStringList others = sharingTheKey(row, resolved);
506 : !others.isEmpty()) {
507 : problems.sharedKey =
508 13 : tr("%1 signs with the same key for another store. A recipient "
509 : "list signed for one of these stores is accepted in the other: "
510 : "use a separate signing key for each store whose recipients "
511 : "differ.")
512 26 : .arg(others.join(QStringLiteral(", ")));
513 : }
514 132 : showProblems(row, problems);
515 132 : status = status && problems.none();
516 132 : }
517 201 : ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(status);
518 201 : updatePassGitWarning();
519 201 : }
520 :
521 298 : void ConfigDialog::updatePassGitWarning() {
522 : // Built after the first applySettings(); setProfiles() fills it then.
523 298 : if (m_passGitWarning == nullptr) {
524 72 : return;
525 : }
526 226 : QStringList warnings;
527 226 : if (ui->radioButtonPass->isChecked()) {
528 22 : QStringList stores{ui->storePath->text()};
529 13 : for (const ProfileEntry &entry : std::as_const(m_entries)) {
530 2 : stores << entry.profile.path;
531 : }
532 : stores.removeDuplicates();
533 24 : for (const QString &store : std::as_const(stores)) {
534 13 : const QString repository = Util::repositoryPassClimbsTo(store);
535 13 : if (!repository.isEmpty()) {
536 5 : warnings << tr("pass commits changes in %1 to the Git repository in "
537 : "%2: with a colon in the store's path, pass cannot "
538 : "keep Git to the store. Run \"git init\" in the store, "
539 : "or use native Git/GPG.")
540 15 : .arg(QDir::toNativeSeparators(store),
541 10 : QDir::toNativeSeparators(repository));
542 : }
543 : }
544 : }
545 452 : m_passGitWarning->setText(warnings.join(QStringLiteral("\n\n")));
546 226 : m_passGitWarning->setVisible(!warnings.isEmpty());
547 11 : }
548 :
549 23 : void ConfigDialog::warnAboutSshAuthSockOverride() {
550 46 : const QString override = ui->sshAuthSockOverride->text().trimmed();
551 23 : if (override.isEmpty()) {
552 : return;
553 : }
554 3 : QString reason;
555 3 : switch (SshAuthSock::overrideStatus(override)) {
556 : case SshAuthSock::OverrideStatus::Valid:
557 : return;
558 1 : case SshAuthSock::OverrideStatus::DoesNotExist:
559 1 : reason = tr("The path does not exist.");
560 1 : break;
561 1 : case SshAuthSock::OverrideStatus::NotReadable:
562 1 : reason = tr("The path is not readable.");
563 1 : break;
564 1 : case SshAuthSock::OverrideStatus::NotUnixDomainSocket:
565 1 : reason = tr("The path is not a Unix domain socket.");
566 1 : break;
567 : }
568 6 : QMessageBox::warning(
569 3 : this, tr("Potentially invalid SSH_AUTH_SOCK override"),
570 3 : tr("The SSH_AUTH_SOCK override value may be invalid.\n\n%1\n\n"
571 : "The value will still be saved as entered.")
572 6 : .arg(reason));
573 : }
574 :
575 23 : void ConfigDialog::followActiveProfile(const Profiles &profiles) {
576 : // As MainWindow::on_profileBox_currentTextChanged does when switching.
577 23 : AppSettings s = QtPassSettings::load();
578 : const QString wasActive = s.activeProfile;
579 23 : QString nowActive;
580 32 : for (const ProfileEntry &entry : std::as_const(m_entries)) {
581 9 : if (!wasActive.isEmpty() && entry.originalName == wasActive) {
582 1 : nowActive = entry.name;
583 : }
584 : }
585 23 : s.activeProfile = nowActive;
586 : const auto active = profiles.constFind(nowActive);
587 23 : if (active != profiles.constEnd()) {
588 1 : s.useGit = active->useGit.value_or(s.useGit);
589 1 : s.autoPush = active->autoPush.value_or(s.autoPush);
590 1 : s.autoPull = active->autoPull.value_or(s.autoPull);
591 : }
592 23 : QtPassSettings::save(s);
593 23 : }
594 :
595 23 : void ConfigDialog::on_accepted() {
596 23 : warnAboutSshAuthSockOverride();
597 23 : const Profiles existingProfiles = QtPassSettings::getProfiles();
598 : // Persist via the facade, which also invalidates the cached Pass backend so
599 : // a changed "use pass" mode takes effect.
600 23 : QtPassSettings::save(readSettings());
601 : // Profiles are not part of AppSettings yet, so persist them separately.
602 23 : const Profiles profiles = getProfiles();
603 23 : QtPassSettings::setProfiles(profiles);
604 23 : followActiveProfile(profiles);
605 23 : initializeNewProfiles(existingProfiles);
606 23 : }
607 :
608 1 : void ConfigDialog::on_autodetectButton_clicked() {
609 2 : QString pass = Util::findBinaryInPath("pass");
610 1 : if (!pass.isEmpty()) {
611 1 : ui->passPath->setText(pass);
612 : }
613 1 : usePass(!pass.isEmpty());
614 2 : QString gpg = Util::findBinaryInPath("gpg2");
615 1 : if (gpg.isEmpty()) {
616 2 : gpg = Util::findBinaryInPath("gpg");
617 : }
618 1 : if (!gpg.isEmpty()) {
619 1 : ui->gpgPath->setText(gpg);
620 : }
621 2 : QString git = Util::findBinaryInPath("git");
622 1 : if (!git.isEmpty()) {
623 1 : ui->gitPath->setText(git);
624 : }
625 2 : QString pwgen = Util::findBinaryInPath("pwgen");
626 1 : if (!pwgen.isEmpty()) {
627 1 : ui->pwgenPath->setText(pwgen);
628 : }
629 1 : }
630 :
631 9 : void ConfigDialog::on_radioButtonNative_clicked() { setGroupBoxState(); }
632 :
633 5 : void ConfigDialog::on_radioButtonPass_clicked() { setGroupBoxState(); }
634 :
635 87 : void ConfigDialog::setGroupBoxState() {
636 87 : bool state = ui->radioButtonPass->isChecked();
637 87 : ui->groupBoxNative->setEnabled(!state);
638 87 : ui->groupBoxPass->setEnabled(state);
639 87 : updatePassGitWarning();
640 87 : on_checkBoxUseGit_clicked();
641 87 : if (state) {
642 8 : ui->spinBoxPasswordLength->setEnabled(false);
643 8 : ui->checkBoxUsePwgen->setEnabled(false);
644 8 : ui->checkBoxAvoidCapitals->setEnabled(false);
645 8 : ui->checkBoxUseSymbols->setEnabled(false);
646 8 : ui->checkBoxLessRandom->setEnabled(false);
647 8 : ui->checkBoxAvoidNumbers->setEnabled(false);
648 8 : ui->labelPasswordChars->setEnabled(false);
649 8 : ui->passwordCharTemplateSelector->setEnabled(false);
650 8 : ui->lineEditPasswordChars->setEnabled(false);
651 : } else {
652 : // native mode: restore pwgen/charset state from existing handlers
653 79 : ui->spinBoxPasswordLength->setEnabled(true);
654 79 : ui->checkBoxUsePwgen->setEnabled(!ui->pwgenPath->text().isEmpty());
655 79 : on_checkBoxUsePwgen_clicked();
656 : }
657 87 : }
658 :
659 8 : auto ConfigDialog::selectExecutable() -> QString {
660 16 : QFileDialog dialog(this);
661 8 : dialog.setFileMode(QFileDialog::ExistingFile);
662 8 : dialog.setOption(QFileDialog::ReadOnly);
663 8 : if (dialog.exec()) {
664 8 : return dialog.selectedFiles().constFirst();
665 : }
666 :
667 4 : return {};
668 8 : }
669 :
670 3 : auto ConfigDialog::selectFolder() -> QString {
671 6 : QFileDialog dialog(this);
672 3 : dialog.setFileMode(QFileDialog::Directory);
673 3 : dialog.setFilter(QDir::NoFilter);
674 3 : dialog.setOption(QFileDialog::ShowDirsOnly);
675 3 : if (dialog.exec()) {
676 4 : return dialog.selectedFiles().constFirst();
677 : }
678 :
679 1 : return {};
680 3 : }
681 :
682 2 : void ConfigDialog::on_toolButtonGit_clicked() {
683 2 : QString git = selectExecutable();
684 2 : bool state = !git.isEmpty();
685 2 : if (state) {
686 1 : ui->gitPath->setText(git);
687 : } else {
688 1 : useGit(false);
689 : }
690 :
691 2 : ui->checkBoxUseGit->setEnabled(state);
692 2 : }
693 :
694 2 : void ConfigDialog::on_toolButtonGpg_clicked() {
695 2 : QString gpg = selectExecutable();
696 2 : if (!gpg.isEmpty()) {
697 1 : ui->gpgPath->setText(gpg);
698 : }
699 2 : }
700 :
701 1 : void ConfigDialog::on_pushButtonGenerateKey_clicked() {
702 1 : KeygenDialog d(ui->gpgPath->text(), QtPassSettings::getPass(), this);
703 1 : d.exec();
704 1 : }
705 :
706 2 : void ConfigDialog::on_toolButtonPass_clicked() {
707 2 : QString pass = selectExecutable();
708 2 : if (!pass.isEmpty()) {
709 1 : ui->passPath->setText(pass);
710 : }
711 2 : }
712 :
713 2 : void ConfigDialog::on_toolButtonStore_clicked() {
714 2 : QString store = selectFolder();
715 2 : if (!store.isEmpty()) {
716 1 : ui->storePath->setText(store);
717 : }
718 2 : }
719 :
720 : // index 0 is "No clipboard".
721 146 : void ConfigDialog::on_comboBoxClipboard_activated(int index) {
722 146 : bool state = index > 0;
723 :
724 146 : ui->checkBoxSelection->setEnabled(state);
725 146 : ui->checkBoxHidePassword->setEnabled(state);
726 146 : updateSecretFieldsEnabled();
727 146 : ui->checkBoxHideContent->setEnabled(state);
728 146 : ui->labelAutoclear->setEnabled(state);
729 146 : ui->spinBoxAutoclearSeconds->setEnabled(state);
730 146 : ui->labelSeconds->setEnabled(state);
731 146 : ui->checkBoxRestoreClipboard->setEnabled(state);
732 146 : }
733 :
734 322 : void ConfigDialog::updateSecretFieldsEnabled() {
735 : // Each control only while the one it depends on is on: other secrets and
736 : // every field only while passwords are hidden; the secret words only while
737 : // other secrets are, and not when every field is hidden anyway.
738 551 : const bool hiding = ui->checkBoxHidePassword->isEnabled() &&
739 229 : ui->checkBoxHidePassword->isChecked();
740 322 : ui->checkBoxHideAllFields->setEnabled(hiding);
741 322 : const bool all = hiding && ui->checkBoxHideAllFields->isChecked();
742 322 : ui->checkBoxHideSecretFields->setEnabled(hiding && !all);
743 : const bool words =
744 322 : hiding && !all && ui->checkBoxHideSecretFields->isChecked();
745 322 : ui->lineEditSecretFields->setEnabled(words);
746 322 : ui->buttonDefaultSecretFields->setEnabled(words);
747 322 : }
748 :
749 74 : void ConfigDialog::useSelection(bool useSelection) {
750 74 : ui->checkBoxSelection->setChecked(useSelection);
751 74 : on_checkBoxSelection_clicked();
752 74 : }
753 :
754 74 : void ConfigDialog::on_checkBoxSelection_clicked() {
755 74 : on_comboBoxClipboard_activated(ui->comboBoxClipboard->currentIndex());
756 74 : }
757 :
758 72 : void ConfigDialog::setProfiles(Profiles profiles,
759 : const QString ¤tProfile) {
760 : // remove weird "" key value pairs
761 72 : profiles.remove(QString());
762 :
763 72 : m_entries.clear();
764 72 : ui->profileList->clear();
765 : int currentRow = -1;
766 96 : for (auto i = profiles.cbegin(); i != profiles.cend(); ++i) {
767 24 : if (i.key() == currentProfile) {
768 9 : currentRow = m_entries.size();
769 : }
770 24 : m_entries.append({i.key(), i.value(), i.key()});
771 24 : ui->profileList->addItem(i.key());
772 : }
773 72 : ui->profileList->setCurrentRow(currentRow);
774 72 : if (currentRow < 0) {
775 63 : loadProfileForm(-1);
776 : }
777 72 : validate();
778 96 : }
779 :
780 226 : auto ConfigDialog::currentEntry() -> ProfileEntry * {
781 226 : if (m_currentEntry < 0 || m_currentEntry >= m_entries.size()) {
782 : return nullptr;
783 : }
784 100 : return &m_entries[m_currentEntry];
785 : }
786 :
787 : // The Git boxes show the profile's own flags, else the global ones; only a
788 : // click on a box makes the value the profile's own.
789 86 : void ConfigDialog::loadProfileForm(int row) {
790 86 : m_currentEntry = (row >= 0 && row < m_entries.size()) ? row : -1;
791 86 : const ProfileEntry *entry = currentEntry();
792 86 : m_loadingForm = true;
793 86 : ui->profileForm->setEnabled(entry != nullptr);
794 86 : ui->deleteButton->setEnabled(entry != nullptr);
795 195 : ui->profileName->setText(entry ? entry->name : QString());
796 195 : ui->profilePath->setText(entry ? entry->profile.path : QString());
797 109 : ui->profileSigningKey->setText(entry ? entry->profile.signingKey : QString());
798 : const bool useGit =
799 109 : entry && entry->profile.useGit.value_or(ui->checkBoxUseGit->isChecked());
800 86 : ui->profileUseGit->setChecked(useGit);
801 86 : ui->profileAutoPush->setChecked(
802 109 : entry &&
803 23 : entry->profile.autoPush.value_or(ui->checkBoxAutoPush->isChecked()));
804 86 : ui->profileAutoPull->setChecked(
805 109 : entry &&
806 23 : entry->profile.autoPull.value_or(ui->checkBoxAutoPull->isChecked()));
807 86 : ui->profileAutoPush->setEnabled(useGit);
808 86 : ui->profileAutoPull->setEnabled(useGit);
809 86 : m_loadingForm = false;
810 86 : updateProfileStatus();
811 86 : validate();
812 86 : }
813 :
814 50 : auto ConfigDialog::getProfiles() -> Profiles {
815 50 : Profiles profiles;
816 72 : for (const ProfileEntry &entry : std::as_const(m_entries)) {
817 22 : profiles.insert(entry.name, entry.profile);
818 : }
819 50 : return profiles;
820 : }
821 :
822 6 : auto ConfigDialog::ensureProfileFolder(const QString &path) -> bool {
823 6 : if (QDir(path).exists()) {
824 : return true;
825 : }
826 4 : if (QMessageBox::question(
827 2 : this, tr("Create profile directory?"),
828 4 : tr("Would you like to create a password store at %1?").arg(path),
829 : QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
830 : return false;
831 : }
832 1 : if (!Util::mkpathForStore(path)) {
833 2 : QMessageBox::warning(
834 1 : this, tr("Error"),
835 2 : tr("Could not create profile directory: %1").arg(path));
836 1 : return false;
837 : }
838 : return true;
839 : }
840 :
841 3 : void ConfigDialog::initialiseProfileStore(const QString &name,
842 : const Profile &profile,
843 : const QString &path) {
844 : // Only the recipients come through the active backend. passStore is the
845 : // profile, or getGpgIdPath() would pre-tick the active store's recipients;
846 : // signing keys are per profile, exactly as the profile switch applies them
847 : // (MainWindow::on_profileBox_currentTextChanged).
848 3 : const AppSettings settings = QtPassSettings::load();
849 3 : AppSettings profileSettings = settings;
850 3 : profileSettings.passStore = Util::normalizeFolderPath(path);
851 3 : profileSettings.passSigningKey = profile.signingKey;
852 : // needsInit() found no .gpg-id: nothing to preselect, and none that
853 : // appears meanwhile is taken (it would be read under the active
854 : // profile's signing policy).
855 : UsersDialog usersDialog(QtPassSettings::getPass(), profileSettings,
856 : profileSettings.passStore,
857 6 : UsersDialog::Preselect::Nothing, this);
858 : usersDialog.setInitOnAccept(false);
859 6 : usersDialog.setWindowTitle(tr("Select recipients for %1").arg(name));
860 3 : if (usersDialog.exec() != QDialog::Accepted) {
861 : return;
862 : }
863 2 : QString note;
864 2 : const bool ok = ProfileInit::initialise(
865 2 : path, usersDialog.selectedUsers(), profileSettings,
866 : profile.useGit.value_or(settings.useGit), ¬e);
867 2 : if (!ok) {
868 2 : QMessageBox::warning(this, tr("Could not initialise profile %1").arg(name),
869 : note);
870 1 : } else if (!note.isEmpty()) {
871 2 : QMessageBox::information(this, tr("Profile %1").arg(name), note);
872 : }
873 3 : }
874 :
875 23 : void ConfigDialog::initializeNewProfiles(const Profiles &existingProfiles) {
876 23 : const Profiles newProfiles = getProfiles();
877 : // QMap iterates in name order.
878 55 : for (auto it = newProfiles.cbegin(); it != newProfiles.cend(); ++it) {
879 9 : const QString path = QDir::cleanPath(it.value().path);
880 : // A store that was already a profile when the dialog opened, under this
881 : // or another name: a rename is not a new store.
882 9 : const bool known = std::any_of(
883 : existingProfiles.cbegin(), existingProfiles.cend(),
884 4 : [&it](const Profile &old) { return old.path == it.value().path; });
885 : // needsInit is false for a missing folder, so it is created first.
886 9 : if (!known && ensureProfileFolder(path) && ProfileInit::needsInit(path)) {
887 3 : initialiseProfileStore(it.key(), it.value(), path);
888 : }
889 : }
890 23 : }
891 :
892 7 : void ConfigDialog::on_addButton_clicked() {
893 7 : ProfileEntry entry;
894 7 : entry.name = tr("New profile");
895 7 : entry.profile.path = ui->storePath->text();
896 7 : m_entries.append(entry);
897 7 : ui->profileList->addItem(entry.name);
898 7 : ui->profileList->setCurrentRow(m_entries.size() - 1);
899 7 : ui->profileName->setFocus();
900 7 : ui->profileName->selectAll();
901 7 : validate();
902 7 : }
903 :
904 1 : void ConfigDialog::on_profilePathBrowse_clicked() {
905 1 : const QString dir = selectFolder();
906 1 : if (!dir.isEmpty()) {
907 1 : ui->profilePath->setText(dir);
908 1 : onProfilePathEdited(dir);
909 : }
910 1 : }
911 :
912 3 : void ConfigDialog::on_deleteButton_clicked() {
913 3 : const int row = ui->profileList->currentRow();
914 3 : if (row < 0 || row >= m_entries.size()) {
915 3 : QMessageBox::warning(this, tr("No profile selected"),
916 1 : tr("No profile selected to delete"));
917 1 : return;
918 : }
919 2 : m_entries.removeAt(row);
920 2 : delete ui->profileList->takeItem(row);
921 : // currentRowChanged has re-pointed the form at the neighbour (or nothing).
922 2 : validate();
923 : }
924 :
925 0 : void ConfigDialog::criticalMessage(const QString &title, const QString &text) {
926 0 : QMessageBox::critical(this, title, text, QMessageBox::Ok, QMessageBox::Ok);
927 0 : }
928 :
929 : // Looks on Util's PATH (with the macOS and Windows additions) without a
930 : // subprocess; a hit is stored for the QR display, a miss leaves the setting.
931 72 : auto ConfigDialog::isQrencodeAvailable(const QString &configuredPath) -> bool {
932 : #ifdef Q_OS_WIN
933 : Q_UNUSED(configuredPath);
934 : return false;
935 : #else
936 72 : if (!configuredPath.isEmpty()) {
937 4 : const QFileInfo configured(configuredPath);
938 4 : if (configured.isFile() && configured.isExecutable()) {
939 : return true;
940 : }
941 4 : }
942 142 : const QString found = Util::findBinaryInPath(QStringLiteral("qrencode"));
943 71 : if (found.isEmpty()) {
944 : return false;
945 : }
946 1 : QtPassSettings::setQrencodeExecutable(found);
947 : return true;
948 : #endif
949 : }
950 :
951 72 : void ConfigDialog::useTrayIcon(bool useSystray) {
952 72 : if (QSystemTrayIcon::isSystemTrayAvailable()) {
953 0 : ui->checkBoxUseTrayIcon->setChecked(useSystray);
954 0 : ui->checkBoxHideOnClose->setEnabled(useSystray);
955 0 : ui->checkBoxStartMinimized->setEnabled(useSystray);
956 :
957 0 : if (!useSystray) {
958 0 : ui->checkBoxHideOnClose->setChecked(false);
959 0 : ui->checkBoxStartMinimized->setChecked(false);
960 : }
961 : }
962 72 : }
963 :
964 2 : void ConfigDialog::on_checkBoxUseTrayIcon_clicked() {
965 2 : bool state = ui->checkBoxUseTrayIcon->isChecked();
966 2 : ui->checkBoxHideOnClose->setEnabled(state);
967 2 : ui->checkBoxStartMinimized->setEnabled(state);
968 2 : }
969 :
970 120 : void ConfigDialog::useGit(bool useGit) {
971 120 : ui->checkBoxUseGit->setChecked(useGit);
972 120 : on_checkBoxUseGit_clicked();
973 120 : }
974 :
975 74 : void ConfigDialog::useOtp(bool useOtp) {
976 74 : ui->checkBoxUseOtp->setChecked(useOtp);
977 74 : }
978 :
979 74 : void ConfigDialog::useGrepSearch(bool useGrepSearch) {
980 74 : ui->checkBoxUseGrepSearch->setChecked(useGrepSearch);
981 74 : }
982 :
983 74 : void ConfigDialog::useQrencode(bool useQrencode) {
984 74 : ui->checkBoxUseQrencode->setChecked(useQrencode);
985 74 : }
986 :
987 207 : void ConfigDialog::on_checkBoxUseGit_clicked() {
988 207 : ui->checkBoxAddGPGId->setEnabled(ui->checkBoxUseGit->isChecked());
989 207 : ui->checkBoxAutoPull->setEnabled(ui->checkBoxUseGit->isChecked());
990 207 : ui->checkBoxAutoPush->setEnabled(ui->checkBoxUseGit->isChecked());
991 : // pass always stops git at the store's parent itself, so the choice only
992 : // means something in native mode.
993 207 : ui->checkBoxGitUseParentRepository->setEnabled(
994 207 : ui->checkBoxUseGit->isChecked() && !ui->radioButtonPass->isChecked());
995 207 : }
996 :
997 2 : void ConfigDialog::on_toolButtonPwgen_clicked() {
998 2 : QString pwgen = selectExecutable();
999 2 : if (!pwgen.isEmpty()) {
1000 1 : ui->pwgenPath->setText(pwgen);
1001 1 : ui->checkBoxUsePwgen->setEnabled(true);
1002 : } else {
1003 1 : ui->checkBoxUsePwgen->setEnabled(false);
1004 1 : ui->checkBoxUsePwgen->setChecked(false);
1005 : }
1006 2 : }
1007 :
1008 74 : void ConfigDialog::setPwgenPath(const QString &pwgen) {
1009 74 : ui->pwgenPath->setText(pwgen);
1010 74 : if (pwgen.isEmpty()) {
1011 48 : ui->checkBoxUsePwgen->setChecked(false);
1012 48 : ui->checkBoxUsePwgen->setEnabled(false);
1013 : }
1014 74 : on_checkBoxUsePwgen_clicked();
1015 74 : }
1016 :
1017 228 : void ConfigDialog::on_checkBoxUsePwgen_clicked() {
1018 228 : if (ui->radioButtonPass->isChecked())
1019 : return;
1020 224 : bool usePwgen = ui->checkBoxUsePwgen->isChecked();
1021 224 : ui->checkBoxAvoidCapitals->setEnabled(usePwgen);
1022 224 : ui->checkBoxAvoidNumbers->setEnabled(usePwgen);
1023 224 : ui->checkBoxLessRandom->setEnabled(usePwgen);
1024 224 : ui->checkBoxUseSymbols->setEnabled(usePwgen);
1025 224 : ui->lineEditPasswordChars->setEnabled(!usePwgen);
1026 224 : ui->labelPasswordChars->setEnabled(!usePwgen);
1027 224 : ui->passwordCharTemplateSelector->setEnabled(!usePwgen);
1028 : }
1029 :
1030 75 : void ConfigDialog::usePwgen(bool usePwgen) {
1031 150 : if (ui->pwgenPath->text().isEmpty()) {
1032 : usePwgen = false;
1033 : }
1034 75 : ui->checkBoxUsePwgen->setChecked(usePwgen);
1035 75 : on_checkBoxUsePwgen_clicked();
1036 75 : }
1037 :
1038 76 : void ConfigDialog::setPasswordConfiguration(
1039 : const PasswordConfiguration &config) {
1040 : // Retain the custom charset so it survives even when a builtin set is the
1041 : // active selection (the line edit then shows the builtin's characters).
1042 76 : m_customPasswordChars = config.Characters[PasswordConfiguration::CUSTOM];
1043 76 : ui->spinBoxPasswordLength->setValue(config.length);
1044 76 : ui->passwordCharTemplateSelector->setCurrentIndex(config.selected);
1045 76 : if (config.selected != PasswordConfiguration::CUSTOM) {
1046 74 : ui->lineEditPasswordChars->setEnabled(false);
1047 : }
1048 76 : ui->lineEditPasswordChars->setText(config.Characters[config.selected]);
1049 76 : }
1050 :
1051 27 : auto ConfigDialog::getPasswordConfiguration() -> PasswordConfiguration {
1052 27 : PasswordConfiguration config;
1053 27 : config.length = ui->spinBoxPasswordLength->value();
1054 27 : config.selected = static_cast<PasswordConfiguration::CharacterSet>(
1055 27 : ui->passwordCharTemplateSelector->currentIndex());
1056 : // For a builtin selection the line edit shows the builtin's characters;
1057 : // reading it would overwrite the saved custom charset.
1058 27 : if (config.selected == PasswordConfiguration::CUSTOM) {
1059 : config.Characters[PasswordConfiguration::CUSTOM] =
1060 4 : ui->lineEditPasswordChars->text();
1061 : } else {
1062 25 : config.Characters[PasswordConfiguration::CUSTOM] = m_customPasswordChars;
1063 : }
1064 27 : return config;
1065 0 : }
1066 :
1067 2 : void ConfigDialog::on_passwordCharTemplateSelector_activated(int index) {
1068 4 : ui->lineEditPasswordChars->setText(
1069 2 : QtPassSettings::getPasswordConfiguration().Characters[index]);
1070 2 : if (index == PasswordConfiguration::CUSTOM) {
1071 1 : ui->lineEditPasswordChars->setEnabled(true);
1072 : } else {
1073 1 : ui->lineEditPasswordChars->setEnabled(false);
1074 : }
1075 2 : }
1076 :
1077 23 : void ConfigDialog::onProfileSelected(int row) { loadProfileForm(row); }
1078 :
1079 11 : void ConfigDialog::onProfileNameEdited(const QString &name) {
1080 11 : ProfileEntry *entry = currentEntry();
1081 11 : if (m_loadingForm || entry == nullptr) {
1082 : return;
1083 : }
1084 11 : entry->name = name;
1085 11 : if (QListWidgetItem *item = ui->profileList->item(m_currentEntry)) {
1086 11 : item->setText(name);
1087 : }
1088 11 : validate();
1089 11 : updateProfileStatus();
1090 : }
1091 :
1092 9 : void ConfigDialog::onProfilePathEdited(const QString &path) {
1093 9 : ProfileEntry *entry = currentEntry();
1094 9 : if (m_loadingForm || entry == nullptr) {
1095 : return;
1096 : }
1097 9 : entry->profile.path = path;
1098 9 : validate();
1099 9 : updateProfileStatus();
1100 : }
1101 :
1102 13 : void ConfigDialog::onProfileSigningKeyEdited(const QString &key) {
1103 13 : ProfileEntry *entry = currentEntry();
1104 13 : if (m_loadingForm || entry == nullptr) {
1105 : return;
1106 : }
1107 13 : entry->profile.signingKey = key;
1108 13 : validate();
1109 : }
1110 :
1111 : // A Git box was clicked: the profile now has its own flags.
1112 1 : void ConfigDialog::onProfileGitToggled() {
1113 1 : ProfileEntry *entry = currentEntry();
1114 1 : if (m_loadingForm || entry == nullptr) {
1115 : return;
1116 : }
1117 1 : entry->profile.useGit = ui->profileUseGit->isChecked();
1118 1 : entry->profile.autoPush = ui->profileAutoPush->isChecked();
1119 1 : entry->profile.autoPull = ui->profileAutoPull->isChecked();
1120 1 : ui->profileAutoPush->setEnabled(ui->profileUseGit->isChecked());
1121 1 : ui->profileAutoPull->setEnabled(ui->profileUseGit->isChecked());
1122 : }
1123 :
1124 106 : void ConfigDialog::updateProfileStatus() {
1125 106 : const ProfileEntry *entry = currentEntry();
1126 106 : if (entry == nullptr) {
1127 63 : ui->statusLabel->setText(QString());
1128 63 : return;
1129 : }
1130 :
1131 43 : QString statusMessage;
1132 43 : if (!entry->name.isEmpty() && !entry->profile.path.isEmpty()) {
1133 40 : const QString path = QDir::cleanPath(entry->profile.path);
1134 40 : if (!QDir(path).exists()) {
1135 36 : statusMessage = tr("New profile: %1 at %2").arg(entry->name, path);
1136 : } else {
1137 44 : statusMessage = tr("Profile: %1 at %2").arg(entry->name, path);
1138 : }
1139 : } else {
1140 3 : statusMessage = tr("Fill in all required fields");
1141 : }
1142 :
1143 43 : ui->statusLabel->setText(statusMessage);
1144 : }
|