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

IJHack / QtPass / 27701586242

17 Jun 2026 03:48PM UTC coverage: 57.087% (-0.02%) from 57.104%
27701586242

push

github

web-flow
refactor(#1511): inject Pass*/AppSettings into UsersDialog (#1558)

* refactor(#1511): inject Pass*/AppSettings into UsersDialog

Constructor gains Pass *pass and const AppSettings &s parameters,
mirroring the pattern established for ImportKeyDialog and PasswordDialog.
Stored as m_pass, m_passStore, m_gpgExe; replaces 8 QtPassSettings
singleton reads:

- loadGpgKeys: getPass()->listKeys() → m_pass->listKeys()
- markSecretKeys: getPass()->listKeys("",true) → m_pass->listKeys
- loadRecipients: getPass()->listKeys (×2) + getPassStore (×2) → members
- accept: getPass()->Init → m_pass->Init
- on_importKeyButton_clicked: getGpgExecutable() → m_gpgExe

Callers updated:
- MainWindow::onUsers / addRecipient: pass QtPassSettings::getPass() +
  QtPassSettings::load()
- ConfigDialog wizard: same — load() returns the temporarily-switched
  passStore already set by setPassStore(cleanPath) on line 784

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

* fix(#1558): assert pass != nullptr in UsersDialog constructor

Q_ASSERT(pass) fires in debug builds if a null backend is injected,
making the precondition explicit with zero cost in release builds.

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

---------

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

0 of 16 new or added lines in 3 files covered. (0.0%)

1 existing line in 1 file now uncovered.

3963 of 6942 relevant lines covered (57.09%)

23.24 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 pass Active Pass backend.
27
 * @param s Application settings snapshot.
28
 * @param dir Password directory
29
 * @param parent
30
 */
NEW
31
UsersDialog::UsersDialog(Pass *pass, const AppSettings &s, QString dir,
×
NEW
32
                         QWidget *parent)
×
NEW
33
    : QDialog(parent), ui(new Ui::UsersDialog), m_pass(pass),
×
34
      m_passStore(s.passStore), m_gpgExe(s.gpgExecutable),
NEW
35
      m_dir(std::move(dir)) {
×
36
  Q_ASSERT(pass);
UNCOV
37
  ui->setupUi(this);
×
38

39
  restoreDialogState();
×
40
  if (!loadGpgKeys()) {
×
41
    connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
×
42
    return;
×
43
  }
44

45
  loadRecipients();
×
46
  populateList();
×
47

48
  connectSignals();
×
49
}
×
50

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

60
  ui->lineEdit->setClearButtonEnabled(true);
×
61
}
×
62

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

80
auto UsersDialog::loadGpgKeys() -> bool {
×
NEW
81
  QList<UserInfo> users = m_pass->listKeys();
×
82
  if (users.isEmpty()) {
×
83
    QMessageBox::critical(parentWidget(), tr("Keylist missing"),
×
84
                          tr("Could not fetch list of available GPG keys"));
×
85
    reject();
×
86
    return false;
87
  }
88

89
  markSecretKeys(users);
×
90

91
  m_userList = users;
92
  return true;
×
93
}
94

95
void UsersDialog::markSecretKeys(QList<UserInfo> &users) {
×
NEW
96
  QList<UserInfo> secret_keys = m_pass->listKeys("", true);
×
97
  QSet<QString> secretKeyIds;
98
  for (const UserInfo &sec : secret_keys) {
×
99
    secretKeyIds.insert(sec.key_id);
×
100
  }
101
  for (auto &user : users) {
×
102
    if (secretKeyIds.contains(user.key_id)) {
×
103
      user.have_secret = true;
×
104
    }
105
  }
106
}
×
107

108
void UsersDialog::loadRecipients() {
×
109
  int count = 0;
×
110
  QStringList recipients = Pass::getRecipientString(
NEW
111
      m_dir.isEmpty() ? "" : m_dir, m_passStore, " ", &count);
×
112

NEW
113
  QList<UserInfo> selectedUsers = m_pass->listKeys(recipients);
×
114
  QSet<QString> selectedKeyIds;
115
  for (const UserInfo &sel : selectedUsers) {
×
116
    selectedKeyIds.insert(sel.key_id);
×
117
  }
118
  for (auto &user : m_userList) {
×
119
    if (selectedKeyIds.contains(user.key_id)) {
×
120
      user.enabled = true;
×
121
    }
122
  }
123

124
  if (count > selectedUsers.size()) {
×
125
    QStringList allRecipients =
NEW
126
        Pass::getRecipientList(m_dir.isEmpty() ? "" : m_dir, m_passStore);
×
127

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

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

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

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

170
/**
171
 * @brief UsersDialog::accept
172
 */
173
void UsersDialog::accept() {
×
NEW
174
  m_pass->Init(m_dir, m_userList);
×
175

176
  QDialog::accept();
×
177
}
×
178

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

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

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

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

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

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

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

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

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

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

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

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

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

364
void UsersDialog::on_importKeyButton_clicked() {
×
NEW
365
  ImportKeyDialog dialog(m_gpgExe, this);
×
366
  if (dialog.exec() != QDialog::Accepted) {
×
367
    return;
368
  }
369

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

379
  if (!loadGpgKeys()) {
×
380
    return;
381
  }
382

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

391
  // Match against the user's stored key_id, not the visible item text.
392
  // gpg can return a 16-char long key id (IMPORTED status line) or a
393
  // 40-char fingerprint (IMPORT_OK status line); compare both directions
394
  // so a long-id match works against a fingerprint hit and vice versa.
395
  for (int i = 0; i < ui->listWidget->count(); ++i) {
×
396
    QListWidgetItem *item = ui->listWidget->item(i);
×
397
    if (item == nullptr) {
×
398
      continue;
×
399
    }
400
    bool ok = false;
×
401
    const int idx = item->data(Qt::UserRole).toInt(&ok);
×
402
    if (!ok || idx < 0 || idx >= m_userList.size()) {
×
403
      continue;
×
404
    }
405
    const QString &keyId = m_userList[idx].key_id;
×
406
    if (keyId.isEmpty()) {
×
407
      continue;
×
408
    }
409
    // Only perform endsWith checks when the suffix being matched is at least
410
    // 16 chars to avoid false positives with short IDs.
411
    if ((keyId.length() >= 16 &&
×
412
         importedKey.endsWith(keyId, Qt::CaseInsensitive)) ||
×
413
        (importedKey.length() >= 16 &&
×
414
         keyId.endsWith(importedKey, Qt::CaseInsensitive))) {
×
415
      ui->listWidget->setCurrentItem(item);
×
416
      break;
×
417
    }
418
  }
419
}
×
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