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

IJHack / QtPass / 35159963146

16 Sep 2026 10:55PM UTC coverage: 72.009% (-0.3%) from 72.336%
35159963146

push

github

web-flow
One "qtpass" logging category instead of dbg() and 69 QT_DEBUG guards (#1785)

* One "qtpass" logging category instead of dbg() and 69 QT_DEBUG guards

debughelper.h promised file:line output "only in debug builds" while
src.pro already compiled qDebug out of release builds, and the #ifdef
blocks spliced control flow (an else in trayicon.cpp, an else-if in
sshauthsock.cpp) and have broken the build before (55149eca1, 0b6a80bdf).

Q_LOGGING_CATEGORY(lcQtPass, "qtpass", QtInfoMsg): every dbg() is a
qCDebug(lcQtPass), the debug-only qWarning()s are qCWarning(lcQtPass), the
guards are gone, and QT_NO_DEBUG_OUTPUT is no longer defined for release,
so a shipped build can produce a trace with
QT_LOGGING_RULES="qtpass.debug=true". FAQ entry and fixing skill updated;
tst_util checks the category is quiet until asked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JuQsrHonihp1nARE7bzstc

* Convert the last bare qDebug(); FAQ: key IDs

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JuQsrHonihp1nARE7bzstc

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

36 of 78 new or added lines in 15 files covered. (46.15%)

23 existing lines in 8 files now uncovered.

5513 of 7656 relevant lines covered (72.01%)

60.03 hits per line

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

63.13
/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 "windowstatestore.h"
9
#include <QApplication>
10
#include <QDateTime>
11
#include <QDialogButtonBox>
12
#include <QKeyEvent>
13
#include <QLineEdit>
14
#include <QListWidget>
15
#include <QMessageBox>
16
#include <QPushButton>
17
#include <QRegularExpression>
18
#include <QSet>
19
#include <QSignalBlocker>
20
#include <QWidget>
21
#include <algorithm>
22
#include <utility>
23

24
#include "qtpasslogging.h"
25
/**
26
 * @brief UsersDialog::UsersDialog basic constructor
27
 * @param pass Active Pass backend.
28
 * @param s Application settings snapshot.
29
 * @param dir Password directory
30
 * @param parent
31
 */
32
UsersDialog::UsersDialog(Pass *pass, const AppSettings &s, QString dir,
8 ✔
33
                         QWidget *parent)
8 ✔
34
    : QDialog(parent), ui(new Ui::UsersDialog), m_pass(pass),
8 ✔
35
      m_passStore(s.passStore), m_gpgExe(s.gpgExecutable),
36
      m_dir(std::move(dir)) {
16 ✔
37
  Q_ASSERT(pass);
38
  ui->setupUi(this);
8 ✔
39

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

46
  loadRecipients();
8 ✔
47
  populateList();
8 ✔
48

49
  connectSignals();
8 ✔
50
}
×
51

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

61
  ui->lineEdit->setClearButtonEnabled(true);
8 ✔
62
}
8 ✔
63

64
/**
65
 * @brief Restore dialog geometry from settings.
66
 */
67
void UsersDialog::restoreDialogState() {
8 ✔
68
  WindowStateStore::attach(*this, QStringLiteral("usersDialog"));
16 ✔
69
}
8 ✔
70

71
auto UsersDialog::loadGpgKeys() -> bool {
8 ✔
72
  QList<UserInfo> users = m_pass->listKeys();
16 ✔
73
  if (users.isEmpty()) {
8 ✔
74
    QMessageBox::critical(parentWidget(), tr("Keylist missing"),
×
75
                          tr("Could not fetch list of available GPG keys"));
×
76
    reject();
×
77
    return false;
78
  }
79

80
  markSecretKeys(users);
8 ✔
81

82
  m_userList = users;
83
  return true;
8 ✔
84
}
85

86
void UsersDialog::markSecretKeys(QList<UserInfo> &users) {
8 ✔
87
  QList<UserInfo> secret_keys = m_pass->listKeys("", true);
16 ✔
88
  QSet<QString> secretKeyIds;
89
  for (const UserInfo &sec : secret_keys) {
10 ✔
90
    secretKeyIds.insert(sec.key_id);
2 ✔
91
  }
92
  for (auto &user : users) {
24 ✔
93
    if (secretKeyIds.contains(user.key_id)) {
16 ✔
94
      user.have_secret = true;
2 ✔
95
    }
96
  }
97
}
8 ✔
98

99
void UsersDialog::loadRecipients() {
8 ✔
100
  const QStringList recipients =
101
      Pass::getRecipientList(m_dir.isEmpty() ? "" : m_dir, m_passStore);
24 ✔
102
  if (recipients.isEmpty()) {
8 ✔
103
    // A folder without .gpg-id (new store, new profile) has no recipients
104
    // yet. Without this, listKeys() with no filter returned the whole
105
    // keyring and every key in it came up pre-selected.
106
    return;
107
  }
108
  const int count = static_cast<int>(recipients.size());
3 ✔
109

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

121
  if (count > selectedUsers.size()) {
3 ✔
122
    const QStringList &allRecipients = recipients;
123

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

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

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

161
/**
162
 * @brief UsersDialog::~UsersDialog basic destructor.
163
 */
164
UsersDialog::~UsersDialog() = default;
16 ✔
165

166
/**
167
 * @brief UsersDialog::accept
168
 */
169
void UsersDialog::accept() {
3 ✔
170
  if (!hasSelection()) {
3 ✔
171
    // Nothing to encrypt to: an empty .gpg-id would make every insert fail
172
    // and the wizard never offers this dialog again once the file exists.
173
    return;
174
  }
175
  if (m_initOnAccept) {
2 ✔
176
    m_pass->Init(m_dir, m_userList);
2 ✔
177
  }
178

179
  QDialog::accept();
2 ✔
180
}
181

182
auto UsersDialog::hasSelection() const -> bool {
19 ✔
183
  return std::any_of(m_userList.cbegin(), m_userList.cend(),
19 ✔
184
                     [](const UserInfo &user) { return user.enabled; });
28 ✔
185
}
186

187
void UsersDialog::updateOkButton() {
16 ✔
188
  if (auto *ok = ui->buttonBox->button(QDialogButtonBox::Ok)) {
16 ✔
189
    ok->setEnabled(hasSelection());
16 ✔
190
  }
191
}
16 ✔
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) {
12 ✔
213
  if (!item) {
12 ✔
214
    return;
6 ✔
215
  }
216
  bool ok = false;
12 ✔
217
  const int index = item->data(Qt::UserRole).toInt(&ok);
12 ✔
218
  if (!ok) {
12 ✔
219
    qCWarning(lcQtPass)
12 ✔
220
        << "UsersDialog::itemChange: invalid user index data for item";
6 ✔
221
    return;
6 ✔
222
  }
223
  if (index < 0 || index >= m_userList.size()) {
6 ✔
NEW
224
    qCWarning(lcQtPass) << "UsersDialog::itemChange: user index out of range:"
×
NEW
225
                        << index << "valid range is [0,"
×
NEW
226
                        << (m_userList.size() - 1) << "]";
×
UNCOV
227
    return;
×
228
  }
229
  m_userList[index].enabled = item->checkState() == Qt::Checked;
6 ✔
230
  updateOkButton();
6 ✔
231
}
232

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

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

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

261
    auto *item = new QListWidgetItem(buildUserText(user), ui->listWidget);
18 ✔
262
    applyUserStyling(item, user);
18 ✔
263
    item->setCheckState(user.enabled ? Qt::Checked : Qt::Unchecked);
32 ✔
264
    item->setData(Qt::UserRole, QVariant::fromValue(i));
18 ✔
265
    ui->listWidget->addItem(item);
18 ✔
266
  }
267
  updateOkButton();
10 ✔
268
}
10 ✔
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,
20 ✔
278
                               const QRegularExpression &nameFilter) const {
279
  if (!filter.isEmpty() && !nameFilter.match(user.name).hasMatch()) {
22 ✔
280
    return false;
281
  }
282
  if (!user.isValid() && !ui->checkBox->isChecked()) {
18 ✔
283
    return false;
284
  }
285
  const bool expired = isUserExpired(user);
18 ✔
286
  return !(expired && !ui->checkBox->isChecked());
18 ✔
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 {
36 ✔
295
  if (!m_cachedDateTimeValid) {
36 ✔
296
    m_cachedCurrentDateTime = QDateTime::currentDateTime();
9 ✔
297
    m_cachedDateTimeValid = true;
9 ✔
298
  }
299
  return user.expiry.isValid() && m_cachedCurrentDateTime > user.expiry;
36 ✔
300
}
301

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

320
/**
321
 * @brief Applies visual styling to a user list item based on key status.
322
 * @param item List widget item to style
323
 * @param user User whose status determines styling
324
 */
325
void UsersDialog::applyUserStyling(QListWidgetItem *item,
18 ✔
326
                                   const UserInfo &user) const {
327
  const QString originalText = item->text();
18 ✔
328
  // Status badge first: an own key can be expired too, and then the marker
329
  // matters more than the "you can decrypt with this" colour.
330
  bool badged = true;
331
  if (!user.isValid()) {
18 ✔
332
    item->setBackground(Qt::darkRed);
×
333
    item->setForeground(Qt::white);
×
334
    item->setText(tr("[INVALID] ") + originalText);
×
335
  } else if (isUserExpired(user)) {
18 ✔
336
    // Same treatment as invalid: a lone dark-red foreground is unreadable on
337
    // dark themes, and gpg refuses to encrypt to expired keys anyway.
338
    item->setBackground(Qt::darkRed);
×
339
    item->setForeground(Qt::white);
×
340
    item->setText(tr("[EXPIRED] ") + originalText);
×
341
  } else if (!user.fullyValid()) {
18 ✔
342
    item->setBackground(Qt::darkYellow);
×
343
    item->setForeground(Qt::white);
×
344
    item->setText(tr("[PARTIAL] ") + originalText);
×
345
  } else {
346
    item->setText(originalText);
18 ✔
347
    badged = false;
348
  }
349
  if (user.have_secret) {
18 ✔
350
    if (!badged) {
4 ✔
351
      item->setForeground(QApplication::palette().color(QPalette::Link));
8 ✔
352
    }
353
    QFont font = item->font();
4 ✔
354
    font.setBold(true);
355
    item->setFont(font);
4 ✔
356
  }
4 ✔
357
}
18 ✔
358

359
/**
360
 * @brief UsersDialog::on_lineEdit_textChanged typing in the searchbox.
361
 * @param filter
362
 */
363
void UsersDialog::on_lineEdit_textChanged(const QString &filter) {
2 ✔
364
  populateList(filter);
2 ✔
365
}
2 ✔
366

367
/**
368
 * @brief UsersDialog::on_checkBox_clicked filtering.
369
 */
370
void UsersDialog::on_checkBox_clicked() { populateList(ui->lineEdit->text()); }
×
371

372
void UsersDialog::on_importKeyButton_clicked() {
×
373
  ImportKeyDialog dialog(m_gpgExe, this);
×
374
  if (dialog.exec() != QDialog::Accepted) {
×
375
    return;
376
  }
377

378
  // dialog.exec() == Accepted is only reachable after a successful import
379
  // (see ImportKeyDialog::importFromString), so importedKeyId() is non-empty
380
  // by construction. Guard anyway: an empty value would make the
381
  // bidirectional endsWith() below match the first listed key.
382
  const QString importedKey = dialog.importedKeyId();
×
383
  if (importedKey.isEmpty()) {
×
384
    return;
385
  }
386

387
  if (!loadGpgKeys()) {
×
388
    return;
389
  }
390

391
  // Clear the filter so the just-imported key is visible. setText("") still
392
  // emits textChanged once, so don't double-populate.
393
  {
394
    const QSignalBlocker blocker(ui->lineEdit);
×
395
    ui->lineEdit->clear();
×
396
  }
×
397
  populateList(QString());
×
398

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

© 2026 Coveralls, Inc