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

IJHack / QtPass / 24308504363

12 Apr 2026 02:05PM UTC coverage: 20.864% (-0.06%) from 20.927%
24308504363

push

github

web-flow
perf: optimize UsersDialog performance (#977)

* fix: replace static variables with member variables in populateList

* refactor: move docstrings to header, add connectSignals declaration

* fix: pass dir by const ref, remove unused Q_DECLARE_METATYPE

* perf: optimize UsersDialog performance

- Fix N+1 query: use bulk listKeys(recipients) for single gpg call
- Build lookup from key_id, name, email to preserve GPG resolution
- Invalidate cached datetime at start of populateList for fresh expiry check
- Cache compiled regex filter with pattern string guard
- Cache QDateTime for expiry checks to avoid repeated currentDateTime() calls

0 of 22 new or added lines in 1 file covered. (0.0%)

1 existing line in 1 file now uncovered.

1111 of 5325 relevant lines covered (20.86%)

7.72 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 "qtpasssettings.h"
5
#include "ui_usersdialog.h"
6
#include <QApplication>
7
#include <QCloseEvent>
8
#include <QDateTime>
9
#include <QKeyEvent>
10
#include <QMessageBox>
11
#include <QRegularExpression>
12
#include <QSet>
13
#include <QWidget>
14
#include <utility>
15

16
#ifdef QT_DEBUG
17
#include "debughelper.h"
18
#endif
19
/**
20
 * @brief UsersDialog::UsersDialog basic constructor
21
 * @param dir Password directory
22
 * @param parent
23
 */
24
UsersDialog::UsersDialog(const QString &dir, QWidget *parent)
×
25
    : QDialog(parent), ui(new Ui::UsersDialog), m_dir(dir) {
×
26

27
  ui->setupUi(this);
×
28

29
  restoreDialogState();
×
30
  if (!loadGpgKeys()) {
×
31
    connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
×
32
    return;
×
33
  }
34

35
  loadRecipients();
×
36
  populateList();
×
37

38
  connectSignals();
×
39
}
×
40

41
void UsersDialog::connectSignals() {
×
42
  connect(ui->buttonBox, &QDialogButtonBox::accepted, this,
×
43
          &UsersDialog::accept);
×
44
  connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
×
45
  connect(ui->listWidget, &QListWidget::itemChanged, this,
×
46
          &UsersDialog::itemChange);
×
47

48
  ui->lineEdit->setClearButtonEnabled(true);
×
49
}
×
50

51
/**
52
 * @brief Restore dialog geometry from settings.
53
 */
54
void UsersDialog::restoreDialogState() {
×
55
  QByteArray savedGeometry = QtPassSettings::getDialogGeometry("usersDialog");
×
56
  bool hasSavedGeometry = !savedGeometry.isEmpty();
57
  if (hasSavedGeometry) {
×
58
    restoreGeometry(savedGeometry);
×
59
  }
60
  if (QtPassSettings::isDialogMaximized("usersDialog")) {
×
61
    showMaximized();
×
62
  } else if (hasSavedGeometry) {
×
63
    move(QtPassSettings::getDialogPos("usersDialog"));
×
64
    resize(QtPassSettings::getDialogSize("usersDialog"));
×
65
  }
66
}
×
67

68
auto UsersDialog::loadGpgKeys() -> bool {
×
69
  QList<UserInfo> users = QtPassSettings::getPass()->listKeys();
×
70
  if (users.isEmpty()) {
×
71
    QMessageBox::critical(parentWidget(), tr("Keylist missing"),
×
72
                          tr("Could not fetch list of available GPG keys"));
×
73
    reject();
×
74
    return false;
75
  }
76

77
  markSecretKeys(users);
×
78

79
  m_userList = users;
80
  return true;
×
81
}
82

83
void UsersDialog::markSecretKeys(QList<UserInfo> &users) {
×
84
  QList<UserInfo> secret_keys = QtPassSettings::getPass()->listKeys("", true);
×
85
  QSet<QString> secretKeyIds;
86
  for (const UserInfo &sec : secret_keys) {
×
87
    secretKeyIds.insert(sec.key_id);
×
88
  }
89
  for (auto &user : users) {
×
90
    if (secretKeyIds.contains(user.key_id)) {
×
91
      user.have_secret = true;
×
92
    }
93
  }
94
}
×
95

96
void UsersDialog::loadRecipients() {
×
97
  int count = 0;
×
98
  QStringList recipients = QtPassSettings::getPass()->getRecipientString(
×
99
      m_dir.isEmpty() ? "" : m_dir, " ", &count);
×
100

101
  QList<UserInfo> selectedUsers =
102
      QtPassSettings::getPass()->listKeys(recipients);
×
103
  QSet<QString> selectedKeyIds;
104
  for (const UserInfo &sel : selectedUsers) {
×
105
    selectedKeyIds.insert(sel.key_id);
×
106
  }
107
  for (auto &user : m_userList) {
×
108
    if (selectedKeyIds.contains(user.key_id)) {
×
109
      user.enabled = true;
×
110
    }
111
  }
112

113
  if (count > selectedUsers.size()) {
×
114
    QStringList allRecipients = QtPassSettings::getPass()->getRecipientList(
×
115
        m_dir.isEmpty() ? "" : m_dir);
×
116

117
    // Use bulk lookup to resolve all recipients at once (single gpg call)
118
    // This preserves the original email/UID resolution behavior
119
    QList<UserInfo> resolvedKeys =
NEW
120
        QtPassSettings::getPass()->listKeys(allRecipients);
×
121
    // Track resolved recipients by their resolved key_id
122
    QSet<QString> resolvedKeyIds;
NEW
123
    for (const UserInfo &key : resolvedKeys) {
×
NEW
124
      resolvedKeyIds.insert(key.key_id);
×
125
    }
126

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

141
    for (const QString &recipient : allRecipients) {
×
NEW
142
      if (!resolvedKeyIds.contains(recipient) &&
×
NEW
143
          !resolvedRecipients.contains(recipient) &&
×
144
          !selectedKeyIds.contains(recipient)) {
145
        UserInfo i;
×
146
        i.enabled = true;
×
147
        i.key_id = recipient;
×
148
        i.name = " ?? " + tr("Key not found in keyring");
×
149
        m_userList.append(i);
150
      }
×
151
    }
152
  }
153
}
×
154

155
/**
156
 * @brief UsersDialog::~UsersDialog basic destructor.
157
 */
158
UsersDialog::~UsersDialog() { delete ui; }
×
159

160
/**
161
 * @brief UsersDialog::accept
162
 */
163
void UsersDialog::accept() {
×
164
  QtPassSettings::getPass()->Init(m_dir, m_userList);
×
165

166
  QDialog::accept();
×
167
}
×
168

169
/**
170
 * @brief UsersDialog::closeEvent save window state on close.
171
 * @param event
172
 */
173
void UsersDialog::closeEvent(QCloseEvent *event) {
×
174
  QtPassSettings::setDialogGeometry("usersDialog", saveGeometry());
×
175
  if (!isMaximized()) {
×
176
    QtPassSettings::setDialogPos("usersDialog", pos());
×
177
    QtPassSettings::setDialogSize("usersDialog", size());
×
178
  }
179
  QtPassSettings::setDialogMaximized("usersDialog", isMaximized());
×
180
  event->accept();
181
}
×
182

183
/**
184
 * @brief UsersDialog::keyPressEvent clear the lineEdit when escape is pressed.
185
 * No action for Enter currently.
186
 * @param event
187
 */
188
void UsersDialog::keyPressEvent(QKeyEvent *event) {
×
189
  switch (event->key()) {
×
190
  case Qt::Key_Escape:
×
191
    ui->lineEdit->clear();
×
192
    break;
×
193
  default:
194
    break;
195
  }
196
}
×
197

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

224
/**
225
 * @brief UsersDialog::populateList update the view based on filter options
226
 * (such as searching).
227
 * @param filter
228
 */
229
void UsersDialog::populateList(const QString &filter) {
×
230
  // Invalidate cached datetime so expiry checks use fresh current time
NEW
231
  m_cachedDateTimeValid = false;
×
232

NEW
233
  QString patternString = "*" + filter + "*";
×
NEW
234
  if (m_cachedPatternString != patternString) {
×
235
    QRegularExpression re(
NEW
236
        QRegularExpression::wildcardToRegularExpression(patternString),
×
UNCOV
237
        QRegularExpression::CaseInsensitiveOption);
×
NEW
238
    if (re.isValid()) {
×
NEW
239
      m_cachedNameFilter = re;
×
NEW
240
      m_cachedPatternString = patternString;
×
241
    }
242
  }
×
243
  const QRegularExpression &nameFilter = m_cachedNameFilter;
×
244
  ui->listWidget->clear();
×
245

246
  for (int i = 0; i < m_userList.size(); ++i) {
×
247
    const auto &user = m_userList.at(i);
248
    if (!passesFilter(user, filter, nameFilter)) {
×
249
      continue;
×
250
    }
251

252
    auto *item = new QListWidgetItem(buildUserText(user), ui->listWidget);
×
253
    applyUserStyling(item, user);
×
254
    item->setCheckState(user.enabled ? Qt::Checked : Qt::Unchecked);
×
255
    item->setData(Qt::UserRole, QVariant::fromValue(i));
×
256
    ui->listWidget->addItem(item);
×
257
  }
258
}
×
259

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

279
/**
280
 * @brief Checks if a user's key has expired.
281
 * @param user User to check
282
 * @return true if user's key is expired
283
 */
284
auto UsersDialog::isUserExpired(const UserInfo &user) const -> bool {
×
NEW
285
  if (!m_cachedDateTimeValid) {
×
NEW
286
    m_cachedCurrentDateTime = QDateTime::currentDateTime();
×
NEW
287
    m_cachedDateTimeValid = true;
×
288
  }
289
  return user.expiry.toSecsSinceEpoch() > 0 &&
×
NEW
290
         m_cachedCurrentDateTime > user.expiry;
×
291
}
292

293
/**
294
 * @brief Builds display text for a user.
295
 * @param user User to format
296
 * @return Formatted user text
297
 */
298
QString UsersDialog::buildUserText(const UserInfo &user) const {
×
299
  QString text = user.name + "\n" + user.key_id;
×
300
  if (user.created.toSecsSinceEpoch() > 0) {
×
301
    text += " " + tr("created") + " " +
×
302
            QLocale::system().toString(user.created, QLocale::ShortFormat);
×
303
  }
304
  if (user.expiry.toSecsSinceEpoch() > 0) {
×
305
    text += " " + tr("expires") + " " +
×
306
            QLocale::system().toString(user.expiry, QLocale::ShortFormat);
×
307
  }
308
  return text;
×
309
}
310

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

341
/**
342
 * @brief UsersDialog::on_lineEdit_textChanged typing in the searchbox.
343
 * @param filter
344
 */
345
void UsersDialog::on_lineEdit_textChanged(const QString &filter) {
×
346
  populateList(filter);
×
347
}
×
348

349
/**
350
 * @brief UsersDialog::on_checkBox_clicked filtering.
351
 */
352
void UsersDialog::on_checkBox_clicked() { populateList(ui->lineEdit->text()); }
×
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