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

IJHack / QtPass / 28647459190

03 Jul 2026 08:09AM UTC coverage: 56.733% (+0.005%) from 56.728%
28647459190

Pull #1605

github

web-flow
Merge 0b8d3b610 into 1b73b85f8
Pull Request #1605: fix: prevent PasswordDialog content duplication and premature-save data loss

3 of 5 new or added lines in 1 file covered. (60.0%)

1 existing line in 1 file now uncovered.

3813 of 6721 relevant lines covered (56.73%)

30.78 hits per line

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

69.57
/src/passworddialog.cpp
1
// SPDX-FileCopyrightText: 2015 Anne Jan Brouwer
2
// SPDX-License-Identifier: GPL-3.0-or-later
3
#include "passworddialog.h"
4
#include "filecontent.h"
5
#include "pass.h"
6
#include "passwordconfiguration.h"
7
#include "qtpasssettings.h"
8
#include "ui_passworddialog.h"
9
#include "util.h"
10
#include <algorithm>
11

12
#include <QHash>
13
#include <QLabel>
14
#include <QLineEdit>
15
#include <QShortcut>
16
#include <utility>
17

18
#ifdef QT_DEBUG
19
#include "debughelper.h"
20
#endif
21

22
/**
23
 * @brief PasswordDialog::PasswordDialog basic constructor.
24
 * @param passConfig configuration constant
25
 * @param parent
26
 */
27
PasswordDialog::PasswordDialog(PasswordConfiguration passConfig,
17✔
28
                               QWidget *parent)
17✔
29
    : QDialog(parent), ui(new Ui::PasswordDialog),
34✔
30
      m_passConfig(std::move(passConfig)), m_pass(QtPassSettings::getPass()) {
17✔
31
  // m_pass is captured once from the singleton here. This constructor is
32
  // only reached from tests; production always uses the two-arg overload
33
  // that receives an explicit Pass* with a stable, caller-controlled lifetime.
34
  m_templating = false;
35
  m_isNew = false;
36

37
  ui->setupUi(this);
17✔
38
  setLength(m_passConfig.length);
17✔
39
  setPasswordCharTemplate(m_passConfig.selected);
17✔
40

41
  connect(m_pass, &Pass::finishedShow, this, &PasswordDialog::setPass);
17✔
42
}
17✔
43

44
/**
45
 * @brief PasswordDialog::PasswordDialog complete constructor.
46
 * @param file
47
 * @param isNew
48
 * @param parent pointer
49
 */
50
PasswordDialog::PasswordDialog(Pass *pass, const AppSettings &s, QString file,
×
51
                               const bool &isNew, QWidget *parent)
×
52
    : QDialog(parent), ui(new Ui::PasswordDialog), m_pass(pass),
×
53
      m_file(std::move(file)), m_isNew(isNew) {
×
54

55
  if (!isNew) {
×
56
    m_pass->Show(m_file);
×
57
  }
58

59
  ui->setupUi(this);
×
60

61
  setWindowTitle(this->windowTitle() + " " + m_file);
×
62
  m_passConfig = s.passwordConfiguration;
×
63
  usePwgen(s.usePwgen);
×
64
  setTemplate(s.passTemplate, s.useTemplate);
×
65

66
  setLength(m_passConfig.length);
×
67
  setPasswordCharTemplate(m_passConfig.selected);
×
68

69
  connect(m_pass, &Pass::finishedShow, this, &PasswordDialog::setPass);
×
70
  connect(m_pass, &Pass::processErrorExit, this, &PasswordDialog::close);
×
71
  connect(this, &PasswordDialog::accepted, this, &PasswordDialog::on_accepted);
×
72
  connect(this, &PasswordDialog::rejected, this, &PasswordDialog::on_rejected);
×
73
}
×
74

75
/**
76
 * @brief PasswordDialog::~PasswordDialog basic destructor.
77
 */
78
PasswordDialog::~PasswordDialog() { delete ui; }
68✔
79

80
/**
81
 * @brief PasswordDialog::on_checkBoxShow_stateChanged hide or show passwords.
82
 * @param arg1
83
 */
84
void PasswordDialog::on_checkBoxShow_stateChanged(int arg1) {
2✔
85
  if (arg1) {
2✔
86
    ui->lineEditPassword->setEchoMode(QLineEdit::Normal);
1✔
87
  } else {
88
    ui->lineEditPassword->setEchoMode(QLineEdit::Password);
1✔
89
  }
90
}
2✔
91

92
/**
93
 * @brief PasswordDialog::on_createPasswordButton_clicked generate a random
94
 * password.
95
 */
96
void PasswordDialog::on_createPasswordButton_clicked() {
×
97
  ui->widget->setEnabled(false);
×
98
  const int currentIndex = ui->passwordTemplateSwitch->currentIndex();
×
99
  if (currentIndex < 0 ||
×
100
      currentIndex >= static_cast<int>(PasswordConfiguration::CHARSETS_COUNT)) {
101
    ui->widget->setEnabled(true);
×
102
    return;
×
103
  }
104

105
  QString newPass = m_pass->generatePassword(
×
106
      static_cast<unsigned int>(ui->spinBox_pwdLength->value()),
×
107
      m_passConfig.Characters[static_cast<PasswordConfiguration::characterSet>(
108
          currentIndex)]);
×
109
  if (!newPass.isEmpty()) {
×
110
    ui->lineEditPassword->setText(newPass);
×
111
  }
112
  ui->widget->setEnabled(true);
×
113
}
114

115
/**
116
 * @brief PasswordDialog::on_accepted handle Ok click for QDialog
117
 */
118
void PasswordDialog::on_accepted() {
×
119
  // For an existing entry, refuse to save until its decrypted content has
120
  // loaded (Show is asynchronous). getPassword() always returns at least a
121
  // newline, so the previous isEmpty() check never fired and clicking OK early
122
  // overwrote the entry with the empty dialog fields.
NEW
123
  if (!m_isNew && !m_contentLoaded) {
×
UNCOV
124
    return;
×
125
  }
126

NEW
127
  QString newValue = getPassword();
×
128
  if (newValue.right(1) != "\n") {
×
129
    newValue += "\n";
×
130
  }
131

132
  m_pass->Insert(m_file, newValue, !m_isNew);
×
133
}
134

135
/**
136
 * @brief PasswordDialog::on_rejected handle Cancel click for QDialog
137
 */
138
void PasswordDialog::on_rejected() { setPassword(QString()); }
×
139

140
/**
141
 * @brief PasswordDialog::setPassword populate the (templated) fields.
142
 * @param password
143
 */
144
void PasswordDialog::setPassword(const QString &password) {
9✔
145
  // Always parse all fields as editable so users can edit any field in the
146
  // password file. This fixes issue #132 where users couldn't edit
147
  // fields without toggling the "Show all fields templated" setting.
148
  FileContent fileContent = FileContent::parse(password, m_fields, true);
9✔
149
  ui->lineEditPassword->setText(fileContent.getPassword());
9✔
150

151
  QWidget *previous = ui->checkBoxShow;
9✔
152
  // first set templated values
153
  NamedValues namedValues = fileContent.getNamedValues();
9✔
154
  for (QLineEdit *line : std::as_const(m_templateLines)) {
10✔
155
    line->setText(namedValues.takeValue(line->objectName()));
2✔
156
    previous = line;
157
  }
158
  // show remaining values (if there are)
159
  // Remove previously created dynamic widgets to prevent duplicates and leaks
160
  for (QLineEdit *line : std::as_const(m_otherLines)) {
9✔
161
    ui->formLayout->removeRow(line);
×
162
  }
163
  m_otherLines.clear();
9✔
164
  for (const NamedValue &nv : std::as_const(namedValues)) {
12✔
165
    auto *line = new QLineEdit();
3✔
166
    line->setObjectName(nv.name);
3✔
167
    line->setText(nv.value);
3✔
168
    ui->formLayout->addRow(new QLabel(nv.name), line);
3✔
169
    setTabOrder(previous, line);
3✔
170
    m_otherLines.append(line);
3✔
171
    previous = line;
172
  }
173

174
  // setPlainText (not insertPlainText) so re-populating replaces the body
175
  // instead of appending a second copy, which would be saved back as
176
  // duplicated content.
177
  ui->plainTextEdit->setPlainText(fileContent.getRemainingData());
18✔
178
}
9✔
179

180
/**
181
 * @brief PasswordDialog::getPassword  join the (templated) fields to a QString
182
 * for writing back.
183
 * @return collapsed password.
184
 */
185
auto PasswordDialog::getPassword() -> QString {
9✔
186
  QString passFile = ui->lineEditPassword->text() + "\n";
18✔
187
  QList<QLineEdit *> allLines(m_templateLines);
188
  allLines.append(m_otherLines);
9✔
189
  for (QLineEdit *line : std::as_const(allLines)) {
13✔
190
    QString text = line->text();
4✔
191
    if (text.isEmpty()) {
4✔
192
      continue;
193
    }
194
    passFile += line->objectName() + ": " + text + "\n";
8✔
195
  }
196
  passFile += ui->plainTextEdit->toPlainText();
18✔
197
  return passFile;
9✔
198
}
199

200
/**
201
 * @brief PasswordDialog::setTemplate set the template and create the fields.
202
 * @param rawFields
203
 */
204
void PasswordDialog::setTemplate(const QString &rawFields, bool useTemplate) {
12✔
205
  m_fields = rawFields.split('\n');
12✔
206
  m_templating = useTemplate;
12✔
207

208
  for (QLineEdit *line : std::as_const(m_templateLines)) {
14✔
209
    ui->formLayout->removeRow(line);
2✔
210
  }
211
  m_templateLines.clear();
12✔
212

213
  // Defensively remove all rows tracked in m_otherLines to prevent accumulation
214
  // when cycling templates or applying new templates after setPassword
215
  for (QLineEdit *line : std::as_const(m_otherLines)) {
12✔
216
    ui->formLayout->removeRow(line);
×
217
  }
218
  m_otherLines.clear();
12✔
219

220
  if (m_templating) {
12✔
221
    QWidget *previous = ui->checkBoxShow;
6✔
222
    for (const QString &field : std::as_const(m_fields)) {
13✔
223
      if (field.isEmpty()) {
7✔
224
        continue;
×
225
      }
226
      auto *line = new QLineEdit();
7✔
227
      auto *label = new QLabel(field);
7✔
228
      line->setObjectName(field);
7✔
229
      ui->formLayout->addRow(label, line);
7✔
230
      setTabOrder(previous, line);
7✔
231
      m_templateLines.append(line);
7✔
232
      previous = line;
233
    }
234
  }
235
}
12✔
236

237
/**
238
 * @brief PasswordDialog::setLength
239
 * PasswordDialog::setLength password length.
240
 * @param length
241
 */
242
void PasswordDialog::setLength(int length) {
19✔
243
  ui->spinBox_pwdLength->setValue(length);
19✔
244
}
19✔
245

246
/**
247
 * @brief PasswordDialog::setPasswordCharTemplate
248
 * PasswordDialog::setPasswordCharTemplate chose the template style.
249
 * @param templateIndex
250
 */
251
void PasswordDialog::setPasswordCharTemplate(int templateIndex) {
19✔
252
  ui->passwordTemplateSwitch->setCurrentIndex(templateIndex);
19✔
253
}
19✔
254

255
/**
256
 * @brief PasswordDialog::usePwgen
257
 * PasswordDialog::usePwgen don't use own password generator.
258
 * @param usePwgen
259
 */
260
void PasswordDialog::usePwgen(bool usePwgen) {
2✔
261
  ui->passwordTemplateSwitch->setDisabled(usePwgen);
2✔
262
  ui->label_characterset->setDisabled(usePwgen);
2✔
263
}
2✔
264

265
/**
266
 * @brief Set available templates from .templates file and apply default.
267
 * @param templates Hash of template name to field list.
268
 * @param defaultTemplate Name of default template to select.
269
 */
270
void PasswordDialog::setAvailableTemplates(
3✔
271
    const QHash<QString, QStringList> &templates,
272
    const QString &defaultTemplate) {
273
  m_availableTemplates = templates;
3✔
274
  QStringList templateNames = templates.keys();
3✔
275
  if (templateNames.isEmpty()) {
3✔
276
    return;
277
  }
278
  std::sort(templateNames.begin(), templateNames.end());
6✔
279
  QString selected = defaultTemplate;
280
  if (!templateNames.contains(selected)) {
3✔
281
    selected = templateNames.first();
×
282
  }
283
  applyTemplate(selected);
3✔
284
}
285

286
/**
287
 * @brief Apply a template by name.
288
 * @param templateName Name of template to apply.
289
 */
290
void PasswordDialog::applyTemplate(const QString &templateName) {
5✔
291
  auto it = m_availableTemplates.constFind(templateName);
5✔
292
  if (it != m_availableTemplates.constEnd()) {
293
    m_currentTemplateName = templateName;
5✔
294
    QString fields = it.value().join("\n");
5✔
295
    setTemplate(fields, true);
5✔
296
  }
297
}
5✔
298

299
/**
300
 * @brief Cycle to next template (Ctrl+T).
301
 */
302
void PasswordDialog::cycleTemplate() {
4✔
303
  if (m_availableTemplates.isEmpty()) {
304
    return;
2✔
305
  }
306
  QStringList names = m_availableTemplates.keys();
2✔
307
  std::sort(names.begin(), names.end());
4✔
308

309
  qsizetype currentIdx = names.indexOf(m_currentTemplateName);
310
  qsizetype nextIdx;
311
  if (currentIdx < 0) {
2✔
312
    nextIdx = 0;
313
  } else {
314
    nextIdx = (currentIdx + 1) % names.size();
2✔
315
  }
316
  applyTemplate(names.at(nextIdx));
2✔
317
}
318

319
/**
320
 * @brief Sets the password from pass show output.
321
 * @param output Output from pass show command
322
 */
323
void PasswordDialog::setPass(const QString &output) {
9✔
324
  setPassword(output);
9✔
325
  m_contentLoaded = true;
9✔
326
  // Only the first Show result — the one for this dialog's entry — is ours.
327
  // Stop listening so a later Show of a different entry (finishedShow is a
328
  // shared Pass signal) cannot overwrite or duplicate these fields.
329
  disconnect(m_pass, &Pass::finishedShow, this, &PasswordDialog::setPass);
9✔
330
}
9✔
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