• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IJHack / QtPass / 27697884856

17 Jun 2026 02:51PM UTC coverage: 57.128% (-0.1%) from 57.238%
27697884856

push

github

web-flow
refactor(#1511): wire passStore into static chain; parameterise Util::configIsValid/getDir (#1555)

* refactor(#1511): wire passStore into static chain; parameterise Util::configIsValid/getDir

- Pass::getGpgIdPath/getRecipientList/getRecipientString now take an
  explicit passStore parameter; removes qtpasssettings.h from pass.cpp
- Util::configIsValid(const AppSettings &) and getDir(..., passStore)
  parameterised; removes qtpasssettings.h from util.cpp
- imitatepass.cpp uses m_settings.passStore at all 4 call sites
- mainwindow.cpp, configdialog.cpp, qtpass.cpp, usersdialog.cpp updated
  at all affected call sites
- tst_util.cpp: PassStoreGuard+setPassStore boilerplate removed from
  static-chain tests; passStore passed directly; configIsValid/getDir
  calls updated with explicit AppSettings / passStore args

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(#1555): normalise fallback path, guard root store, update stale doc examples

- pass.cpp getGpgIdPath: use normalizedStore (not raw passStore) in the
  !found fallback so both branches always return a forward-slash path;
  guard the startsWith boundary check against root-store ("/") causing a
  "//" prefix by only appending "/" when cleanPassStore doesn't already
  end with one
- util.cpp: update @example and @param blocks for configIsValid and
  getDir to match their current signatures (AppSettings + passStore args)
- tst_util.cpp getGpgIdPathSubfolder: apply QDir::cleanPath to both
  sides of the comparison for cross-platform path separator consistency

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* style: clang-format mainwindow.cpp, pass.h, usersdialog.cpp, util.h

Reflow long Util::getDir / Pass::getRecipientString / getGpgIdPath call
sites that exceeded the 80-column limit after the passStore parameter was
added in the previous commit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

23 of 62 new or added lines in 7 files covered. (37.1%)

14 existing lines in 2 files now uncovered.

3963 of 6937 relevant lines covered (57.13%)

23.21 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/usersdialog.cpp
1
// SPDX-FileCopyrightText: 2015 Anne Jan Brouwer
2
// SPDX-License-Identifier: GPL-3.0-or-later
3
#include "usersdialog.h"
4
#include "importkeydialog.h"
5
#include "pass.h"
6
#include "qtpasssettings.h"
7
#include "ui_usersdialog.h"
8
#include <QApplication>
9
#include <QCloseEvent>
10
#include <QDateTime>
11
#include <QKeyEvent>
12
#include <QLineEdit>
13
#include <QListWidget>
14
#include <QMessageBox>
15
#include <QRegularExpression>
16
#include <QSet>
17
#include <QSignalBlocker>
18
#include <QWidget>
19
#include <utility>
20

21
#ifdef QT_DEBUG
22
#include "debughelper.h"
23
#endif
24
/**
25
 * @brief UsersDialog::UsersDialog basic constructor
26
 * @param dir Password directory
27
 * @param parent
28
 */
29
UsersDialog::UsersDialog(QString dir, QWidget *parent)
×
30
    : QDialog(parent), ui(new Ui::UsersDialog), m_dir(std::move(dir)) {
×
31

32
  ui->setupUi(this);
×
33

34
  restoreDialogState();
×
35
  if (!loadGpgKeys()) {
×
36
    connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
×
37
    return;
×
38
  }
39

40
  loadRecipients();
×
41
  populateList();
×
42

43
  connectSignals();
×
44
}
×
45

46
void UsersDialog::connectSignals() {
×
47
  connect(ui->buttonBox, &QDialogButtonBox::accepted, this,
×
48
          &UsersDialog::accept);
×
49
  connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
×
50
  connect(ui->listWidget, &QListWidget::itemChanged, this,
×
51
          &UsersDialog::itemChange);
×
52
  // importKeyButton is wired via Qt's automatic on_<name>_<signal> mechanism
53
  // already triggered by setupUi().
54

55
  ui->lineEdit->setClearButtonEnabled(true);
×
56
}
×
57

58
/**
59
 * @brief Restore dialog geometry from settings.
60
 */
61
void UsersDialog::restoreDialogState() {
×
62
  QByteArray savedGeometry = QtPassSettings::getDialogGeometry("usersDialog");
×
63
  bool hasSavedGeometry = !savedGeometry.isEmpty();
64
  if (hasSavedGeometry) {
×
65
    restoreGeometry(savedGeometry);
×
66
  }
67
  if (QtPassSettings::isDialogMaximized("usersDialog")) {
×
68
    showMaximized();
×
69
  } else if (hasSavedGeometry) {
×
70
    move(QtPassSettings::getDialogPos("usersDialog"));
×
71
    resize(QtPassSettings::getDialogSize("usersDialog"));
×
72
  }
73
}
×
74

75
auto UsersDialog::loadGpgKeys() -> bool {
×
76
  QList<UserInfo> users = QtPassSettings::getPass()->listKeys();
×
77
  if (users.isEmpty()) {
×
78
    QMessageBox::critical(parentWidget(), tr("Keylist missing"),
×
79
                          tr("Could not fetch list of available GPG keys"));
×
80
    reject();
×
81
    return false;
82
  }
83

84
  markSecretKeys(users);
×
85

86
  m_userList = users;
87
  return true;
×
88
}
89

90
void UsersDialog::markSecretKeys(QList<UserInfo> &users) {
×
91
  QList<UserInfo> secret_keys = QtPassSettings::getPass()->listKeys("", true);
×
92
  QSet<QString> secretKeyIds;
93
  for (const UserInfo &sec : secret_keys) {
×
94
    secretKeyIds.insert(sec.key_id);
×
95
  }
96
  for (auto &user : users) {
×
97
    if (secretKeyIds.contains(user.key_id)) {
×
98
      user.have_secret = true;
×
99
    }
100
  }
101
}
×
102

103
void UsersDialog::loadRecipients() {
×
104
  int count = 0;
×
105
  QStringList recipients =
NEW
106
      Pass::getRecipientString(m_dir.isEmpty() ? "" : m_dir,
×
NEW
107
                               QtPassSettings::getPassStore(), " ", &count);
×
108

109
  QList<UserInfo> selectedUsers =
110
      QtPassSettings::getPass()->listKeys(recipients);
×
111
  QSet<QString> selectedKeyIds;
112
  for (const UserInfo &sel : selectedUsers) {
×
113
    selectedKeyIds.insert(sel.key_id);
×
114
  }
115
  for (auto &user : m_userList) {
×
116
    if (selectedKeyIds.contains(user.key_id)) {
×
117
      user.enabled = true;
×
118
    }
119
  }
120

121
  if (count > selectedUsers.size()) {
×
122
    QStringList allRecipients = Pass::getRecipientList(
NEW
123
        m_dir.isEmpty() ? "" : m_dir, QtPassSettings::getPassStore());
×
124

125
    // Use bulk lookup to resolve all recipients at once (single gpg call)
126
    // This preserves the original email/UID resolution behavior
127
    QList<UserInfo> resolvedKeys =
128
        QtPassSettings::getPass()->listKeys(allRecipients);
×
129
    // Track resolved recipients by their resolved key_id
130
    QSet<QString> resolvedKeyIds;
131
    for (const UserInfo &key : resolvedKeys) {
×
132
      resolvedKeyIds.insert(key.key_id);
×
133
    }
134

135
    // Accept a recipient as resolved if GPG returned a key for it
136
    // (either its exact key_id, or GPG resolved email/UID/fingerprint to it)
137
    QSet<QString> resolvedRecipients;
138
    for (const UserInfo &key : resolvedKeys) {
×
139
      resolvedRecipients.insert(key.key_id);
×
140
      // Also add the name (email/UID) of resolved keys as valid resolved tokens
141
      // since GPG matched them to this key
142
      if (!key.name.isEmpty()) {
×
143
        resolvedRecipients.insert(
×
144
            key.name.section('@', 0, 0));    // email local part
×
145
        resolvedRecipients.insert(key.name); // full email/UID
×
146
      }
147
    }
148

149
    for (const QString &recipient : allRecipients) {
×
150
      if (!resolvedKeyIds.contains(recipient) &&
×
151
          !resolvedRecipients.contains(recipient) &&
×
152
          !selectedKeyIds.contains(recipient)) {
153
        UserInfo i;
×
154
        i.enabled = true;
×
155
        i.key_id = recipient;
×
156
        i.name = " ?? " + tr("Key not found in keyring");
×
157
        m_userList.append(i);
158
      }
×
159
    }
160
  }
161
}
×
162

163
/**
164
 * @brief UsersDialog::~UsersDialog basic destructor.
165
 */
166
UsersDialog::~UsersDialog() { delete ui; }
×
167

168
/**
169
 * @brief UsersDialog::accept
170
 */
171
void UsersDialog::accept() {
×
172
  QtPassSettings::getPass()->Init(m_dir, m_userList);
×
173

174
  QDialog::accept();
×
175
}
×
176

177
/**
178
 * @brief UsersDialog::closeEvent save window state on close.
179
 * @param event
180
 */
181
void UsersDialog::closeEvent(QCloseEvent *event) {
×
182
  QtPassSettings::setDialogGeometry("usersDialog", saveGeometry());
×
183
  if (!isMaximized()) {
×
184
    QtPassSettings::setDialogPos("usersDialog", pos());
×
185
    QtPassSettings::setDialogSize("usersDialog", size());
×
186
  }
187
  QtPassSettings::setDialogMaximized("usersDialog", isMaximized());
×
188
  event->accept();
189
}
×
190

191
/**
192
 * @brief UsersDialog::keyPressEvent clear the lineEdit when escape is pressed.
193
 * No action for Enter currently.
194
 * @param event
195
 */
196
void UsersDialog::keyPressEvent(QKeyEvent *event) {
×
197
  switch (event->key()) {
×
198
  case Qt::Key_Escape:
×
199
    ui->lineEdit->clear();
×
200
    break;
×
201
  default:
202
    break;
203
  }
204
}
×
205

206
/**
207
 * @brief UsersDialog::itemChange update the item information.
208
 * @param item
209
 */
210
void UsersDialog::itemChange(QListWidgetItem *item) {
×
211
  if (!item) {
×
212
    return;
×
213
  }
214
  bool ok = false;
×
215
  const int index = item->data(Qt::UserRole).toInt(&ok);
×
216
  if (!ok) {
×
217
#ifdef QT_DEBUG
218
    qWarning() << "UsersDialog::itemChange: invalid user index data for item";
219
#endif
220
    return;
221
  }
222
  if (index < 0 || index >= m_userList.size()) {
×
223
#ifdef QT_DEBUG
224
    qWarning() << "UsersDialog::itemChange: user index out of range:" << index
225
               << "valid range is [0," << (m_userList.size() - 1) << "]";
226
#endif
227
    return;
228
  }
229
  m_userList[index].enabled = item->checkState() == Qt::Checked;
×
230
}
231

232
/**
233
 * @brief UsersDialog::populateList update the view based on filter options
234
 * (such as searching).
235
 * @param filter
236
 */
237
void UsersDialog::populateList(const QString &filter) {
×
238
  // Invalidate cached datetime so expiry checks use fresh current time
239
  m_cachedDateTimeValid = false;
×
240

241
  QString patternString = "*" + filter + "*";
×
242
  if (m_cachedPatternString != patternString) {
×
243
    QRegularExpression re(
244
        QRegularExpression::wildcardToRegularExpression(patternString),
×
245
        QRegularExpression::CaseInsensitiveOption);
×
246
    if (re.isValid()) {
×
247
      m_cachedNameFilter = re;
×
248
      m_cachedPatternString = patternString;
×
249
    }
250
  }
×
251
  const QRegularExpression &nameFilter = m_cachedNameFilter;
×
252
  ui->listWidget->clear();
×
253

254
  for (int i = 0; i < m_userList.size(); ++i) {
×
255
    const auto &user = m_userList.at(i);
256
    if (!passesFilter(user, filter, nameFilter)) {
×
257
      continue;
×
258
    }
259

260
    auto *item = new QListWidgetItem(buildUserText(user), ui->listWidget);
×
261
    applyUserStyling(item, user);
×
262
    item->setCheckState(user.enabled ? Qt::Checked : Qt::Unchecked);
×
263
    item->setData(Qt::UserRole, QVariant::fromValue(i));
×
264
    ui->listWidget->addItem(item);
×
265
  }
266
}
×
267

268
/**
269
 * @brief Checks if a user passes the filter criteria.
270
 * @param user User to check
271
 * @param filter Filter string
272
 * @param nameFilter Compiled name filter regex
273
 * @return true if user passes filter
274
 */
275
bool UsersDialog::passesFilter(const UserInfo &user, const QString &filter,
×
276
                               const QRegularExpression &nameFilter) const {
277
  if (!filter.isEmpty() && !nameFilter.match(user.name).hasMatch()) {
×
278
    return false;
279
  }
280
  if (!user.isValid() && !ui->checkBox->isChecked()) {
×
281
    return false;
282
  }
283
  const bool expired = isUserExpired(user);
×
284
  return !(expired && !ui->checkBox->isChecked());
×
285
}
286

287
/**
288
 * @brief Checks if a user's key has expired.
289
 * @param user User to check
290
 * @return true if user's key is expired
291
 */
292
auto UsersDialog::isUserExpired(const UserInfo &user) const -> bool {
×
293
  if (!m_cachedDateTimeValid) {
×
294
    m_cachedCurrentDateTime = QDateTime::currentDateTime();
×
295
    m_cachedDateTimeValid = true;
×
296
  }
297
  return user.expiry.toSecsSinceEpoch() > 0 &&
×
298
         m_cachedCurrentDateTime > user.expiry;
×
299
}
300

301
/**
302
 * @brief Builds display text for a user.
303
 * @param user User to format
304
 * @return Formatted user text
305
 */
306
QString UsersDialog::buildUserText(const UserInfo &user) const {
×
307
  QString text = user.name + "\n" + user.key_id;
×
308
  if (user.created.toSecsSinceEpoch() > 0) {
×
309
    text += " " + tr("created") + " " +
×
310
            QLocale::system().toString(user.created, QLocale::ShortFormat);
×
311
  }
312
  if (user.expiry.toSecsSinceEpoch() > 0) {
×
313
    text += " " + tr("expires") + " " +
×
314
            QLocale::system().toString(user.expiry, QLocale::ShortFormat);
×
315
  }
316
  return text;
×
317
}
318

319
/**
320
 * @brief Applies visual styling to a user list item based on key status.
321
 * @param item List widget item to style
322
 * @param user User whose status determines styling
323
 */
324
void UsersDialog::applyUserStyling(QListWidgetItem *item,
×
325
                                   const UserInfo &user) const {
326
  const QString originalText = item->text();
×
327
  if (user.have_secret) {
×
328
    const QPalette palette = QApplication::palette();
×
329
    item->setForeground(palette.color(QPalette::Link));
×
330
    QFont font = item->font();
×
331
    font.setBold(true);
332
    item->setFont(font);
×
333
  } else if (!user.isValid()) {
×
334
    item->setBackground(Qt::darkRed);
×
335
    item->setForeground(Qt::white);
×
336
    item->setText(tr("[INVALID] ") + originalText);
×
337
  } else if (isUserExpired(user)) {
×
338
    item->setForeground(Qt::darkRed);
×
339
    item->setText(tr("[EXPIRED] ") + originalText);
×
340
  } else if (!user.fullyValid()) {
×
341
    item->setBackground(Qt::darkYellow);
×
342
    item->setForeground(Qt::white);
×
343
    item->setText(tr("[PARTIAL] ") + originalText);
×
344
  } else {
345
    item->setText(originalText);
×
346
  }
347
}
×
348

349
/**
350
 * @brief UsersDialog::on_lineEdit_textChanged typing in the searchbox.
351
 * @param filter
352
 */
353
void UsersDialog::on_lineEdit_textChanged(const QString &filter) {
×
354
  populateList(filter);
×
355
}
×
356

357
/**
358
 * @brief UsersDialog::on_checkBox_clicked filtering.
359
 */
360
void UsersDialog::on_checkBox_clicked() { populateList(ui->lineEdit->text()); }
×
361

362
void UsersDialog::on_importKeyButton_clicked() {
×
363
  ImportKeyDialog dialog(QtPassSettings::getGpgExecutable(), this);
×
364
  if (dialog.exec() != QDialog::Accepted) {
×
365
    return;
366
  }
367

368
  // dialog.exec() == Accepted is only reachable after a successful import
369
  // (see ImportKeyDialog::importFromString), so importedKeyId() is non-empty
370
  // by construction. Guard anyway: an empty value would make the
371
  // bidirectional endsWith() below match the first listed key.
372
  const QString importedKey = dialog.importedKeyId();
×
373
  if (importedKey.isEmpty()) {
×
374
    return;
375
  }
376

377
  if (!loadGpgKeys()) {
×
378
    return;
379
  }
380

381
  // Clear the filter so the just-imported key is visible. setText("") still
382
  // emits textChanged once, so don't double-populate.
383
  {
384
    const QSignalBlocker blocker(ui->lineEdit);
×
385
    ui->lineEdit->clear();
×
386
  }
×
387
  populateList(QString());
×
388

389
  // Match against the user's stored key_id, not the visible item text.
390
  // gpg can return a 16-char long key id (IMPORTED status line) or a
391
  // 40-char fingerprint (IMPORT_OK status line); compare both directions
392
  // so a long-id match works against a fingerprint hit and vice versa.
393
  for (int i = 0; i < ui->listWidget->count(); ++i) {
×
394
    QListWidgetItem *item = ui->listWidget->item(i);
×
395
    if (item == nullptr) {
×
396
      continue;
×
397
    }
398
    bool ok = false;
×
399
    const int idx = item->data(Qt::UserRole).toInt(&ok);
×
400
    if (!ok || idx < 0 || idx >= m_userList.size()) {
×
401
      continue;
×
402
    }
403
    const QString &keyId = m_userList[idx].key_id;
×
404
    if (keyId.isEmpty()) {
×
405
      continue;
×
406
    }
407
    // Only perform endsWith checks when the suffix being matched is at least
408
    // 16 chars to avoid false positives with short IDs.
409
    if ((keyId.length() >= 16 &&
×
410
         importedKey.endsWith(keyId, Qt::CaseInsensitive)) ||
×
411
        (importedKey.length() >= 16 &&
×
412
         keyId.endsWith(importedKey, Qt::CaseInsensitive))) {
×
413
      ui->listWidget->setCurrentItem(item);
×
414
      break;
×
415
    }
416
  }
417
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc