QtPass Developer documentation
QtPass test coverage
Current view: top level - src/configdialog.cpp (source / functions) Coverage Total Hit
Test: QtPass Lines: 97.4 % 724 705
Test Date: 2026-11-14 13:19:18 Functions: 98.6 % 70 69

            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 &currentProfile) {
     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), &note);
     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              : }
        

Generated by: LCOV version 2.0-1