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

IJHack / QtPass / 24940894757

25 Apr 2026 09:18PM UTC coverage: 28.024% (+0.4%) from 27.609%
24940894757

push

github

web-flow
feat: add Import key dialog to UsersDialog for issue #1167 (#1170)

* feat: add Import key dialog to UsersDialog for issue #1167

- Add ImportKeyDialog class for importing GPG keys from file or clipboard
- Add Import key button to UsersDialog next to search
- Wire up gpg --import execution
- On success, refresh key list and select newly imported key
- Use --status-fd for machine-readable output
- Parse multiline gpg output correctly (line-by-line)
- Fix QDialogButtonBox to only have Cancel button
- Move on_importKeyButton_clicked to private slots
- Clear filter before showing new key (leave cleared after import)
- Add gpgExe fallback to gpg
- Fix clang-format issues

* fix: address review feedback on Import key dialog

Locale-aware parsing, naming, UX, and tests:

- Rename ImportKeyDialog::importedKeyFingerprint() to importedKeyId()
  and update the lone caller in UsersDialog. The parser returns whatever
  gpg gave us (fingerprint when --status-fd's IMPORT_OK is present,
  long key id when only IMPORTED is, the human-readable token
  otherwise); the new name matches m_importedKeyId and the surrounding
  documentation.

- parseGpgImportOutput now prefers locale-independent status lines:
  [GNUPG:] IMPORT_OK <reason> <fingerprint> first, [GNUPG:] IMPORTED
  <keyid> next, falling back to the English-locale "gpg: key X:
  imported" line. Drops the redundant KEY_IMPORTED_EXACT regex (the
  generalised one matched the same inputs).

- Stop wiping inputTextEdit when the clipboard is empty in
  on_pasteButton_clicked.

- Reject non-armored files in on_fileButton_clicked instead of
  silently corrupting binary keyrings through QString::fromUtf8.
  File picker now advertises ASCII-armored only (*.asc); users with
  binary keys are pointed at gpg --armor --export or the From
  Clipboard path.

- Drop the redundant ui->inputTextEdit->setPlaceholderText() runtime
  set; the .ui already declares it, so translators only see the
  string once.

- UsersDialog::on_impo... (continued)

25 of 113 new or added lines in 3 files covered. (22.12%)

36 existing lines in 2 files now uncovered.

1805 of 6441 relevant lines covered (28.02%)

27.56 hits per line

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

29.76
/src/importkeydialog.cpp
1
// SPDX-FileCopyrightText: 2026 Anne Jan Brouwer
2
// SPDX-License-Identifier: GPL-3.0-or-later
3
#include "importkeydialog.h"
4

5
#include "executor.h"
6
#include "qtpasssettings.h"
7
#include "ui_importkeydialog.h"
8

9
#include <QApplication>
10
#include <QClipboard>
11
#include <QFile>
12
#include <QFileDialog>
13
#include <QMessageBox>
14
#include <QPlainTextEdit>
15
#include <QRegularExpression>
16
#include <QString>
17

18
// Locale-independent: gpg's machine-readable status output via --status-fd 1.
19
// See doc/DETAILS in the GnuPG source for IMPORT_OK / IMPORTED grammar.
20
static const QRegularExpression
21
    IMPORT_OK_RE(QStringLiteral(R"(\[GNUPG:\] IMPORT_OK \d+ ([0-9A-Fa-f]+))"));
22
static const QRegularExpression
23
    IMPORTED_RE(QStringLiteral(R"(\[GNUPG:\] IMPORTED ([0-9A-Fa-f]{16}))"));
24
// Fallback for the human-readable (English-locale) line.
25
static const QRegularExpression KEY_IMPORTED_FALLBACK(
26
    QStringLiteral(R"(gpg: key ([0-9A-Fa-f]{40}|[0-9A-Fa-f]{16}):.*imported)"));
27

28
ImportKeyDialog::ImportKeyDialog(QWidget *parent)
2✔
29
    : QDialog(parent), ui(new Ui::ImportKeyDialog) {
2✔
30
  ui->setupUi(this);
2✔
31
  ui->importButton->setEnabled(false);
2✔
32
}
2✔
33

34
ImportKeyDialog::~ImportKeyDialog() = default;
2✔
35

36
auto ImportKeyDialog::importedKeyId() const -> QString {
1✔
37
  return m_importedKeyId;
1✔
38
}
39

NEW
40
void ImportKeyDialog::on_fileButton_clicked() {
×
41
  const QString fileName = QFileDialog::getOpenFileName(
NEW
42
      this, tr("Import GPG Key"), QString(),
×
NEW
43
      tr("ASCII-armored GPG key") + " (*.asc);;" + tr("All Files") + " (*)");
×
44

NEW
45
  if (fileName.isEmpty()) {
×
46
    return;
47
  }
48

NEW
49
  QFile file(fileName);
×
NEW
50
  if (!file.open(QIODevice::ReadOnly)) {
×
NEW
51
    QMessageBox::warning(this, tr("Import Key"),
×
NEW
52
                         tr("Could not open file: %1").arg(fileName));
×
NEW
53
    return;
×
54
  }
55

NEW
56
  const QByteArray bytes = file.readAll();
×
NEW
57
  file.close();
×
58

59
  // Only ASCII-armored content is shown in the text edit; binary keyrings
60
  // would lose bytes through UTF-8 conversion. Reject anything that doesn't
61
  // start with the PGP armor header.
NEW
62
  if (!bytes.trimmed().startsWith("-----BEGIN PGP")) {
×
63
    // Message body is rich text (uses <code>/<b>); escape the path so any
64
    // characters in it cannot reach the HTML subset Qt renders.
NEW
65
    QMessageBox::warning(
×
NEW
66
        this, tr("Import Key"),
×
NEW
67
        tr("%1 does not look like an ASCII-armored GPG key. Convert it with "
×
68
           "<code>gpg --armor --export</code> first, or paste the armored "
69
           "block via <b>From Clipboard</b>.")
NEW
70
            .arg(fileName.toHtmlEscaped()));
×
71
    return;
72
  }
73

NEW
74
  ui->inputTextEdit->setPlainText(QString::fromUtf8(bytes));
×
NEW
75
}
×
76

NEW
77
void ImportKeyDialog::on_pasteButton_clicked() {
×
NEW
78
  const QClipboard *clipboard = QApplication::clipboard();
×
NEW
79
  const QString text = clipboard->text();
×
NEW
80
  if (text.isEmpty()) {
×
81
    // Don't wipe whatever the user already typed/loaded for an empty
82
    // clipboard.
83
    return;
84
  }
NEW
85
  ui->inputTextEdit->setPlainText(text);
×
86
}
87

NEW
88
void ImportKeyDialog::on_importButton_clicked() {
×
NEW
89
  const QString input = ui->inputTextEdit->toPlainText().trimmed();
×
NEW
90
  if (input.isEmpty()) {
×
91
    return;
92
  }
93

NEW
94
  if (importFromString(input)) {
×
NEW
95
    accept();
×
96
  }
97
}
98

NEW
99
void ImportKeyDialog::on_inputTextEdit_textChanged() {
×
NEW
100
  const bool hasInput = !ui->inputTextEdit->toPlainText().trimmed().isEmpty();
×
NEW
101
  ui->importButton->setEnabled(hasInput);
×
NEW
102
}
×
103

NEW
104
bool ImportKeyDialog::importFromString(const QString &input) {
×
NEW
105
  QString gpgExe = QtPassSettings::getGpgExecutable();
×
NEW
106
  if (gpgExe.isEmpty()) {
×
NEW
107
    gpgExe = "gpg";
×
108
  }
NEW
109
  QStringList args = {"--status-fd", "1", "--import", "--batch", "--yes"};
×
110

NEW
111
  QString stdOut;
×
NEW
112
  QString stdErr;
×
113

NEW
114
  int result = Executor::executeBlocking(gpgExe, args, input, &stdOut, &stdErr);
×
115

NEW
116
  if (result != 0) {
×
NEW
117
    showError(tr("GPG import failed:\n%1").arg(stdErr.toHtmlEscaped()));
×
NEW
118
    return false;
×
119
  }
120

NEW
121
  QString keyId = parseGpgImportOutput(stdOut);
×
NEW
122
  if (keyId.isEmpty()) {
×
NEW
123
    keyId = parseGpgImportOutput(stdErr);
×
124
  }
125

NEW
126
  if (keyId.isEmpty()) {
×
NEW
127
    showError(tr("Could not parse imported key id from GPG output."));
×
NEW
128
    return false;
×
129
  }
130

NEW
131
  m_importedKeyId = keyId;
×
NEW
132
  showSuccess(keyId);
×
133
  return true;
NEW
134
}
×
135

136
auto ImportKeyDialog::parseGpgImportOutput(const QString &output) -> QString {
12✔
137
  const QStringList lines = output.split('\n', Qt::SkipEmptyParts);
12✔
138
  // The order here is the priority order: prefer the locale-independent
139
  // status lines over the English human-readable line, and within the
140
  // status lines prefer IMPORT_OK (full fingerprint) over IMPORTED
141
  // (long key id). Each regex is scanned across every line before we
142
  // fall through to the next regex; the per-line ordering of gpg's
143
  // output (which emits IMPORTED before IMPORT_OK) must not pick the
144
  // weaker identifier.
145
  for (const QString &line : lines) {
20✔
146
    const QRegularExpressionMatch match = IMPORT_OK_RE.match(line);
14✔
147
    if (match.hasMatch()) {
14✔
148
      return match.captured(1);
6✔
149
    }
150
  }
14✔
151
  for (const QString &line : lines) {
10✔
152
    const QRegularExpressionMatch match = IMPORTED_RE.match(line);
5✔
153
    if (match.hasMatch()) {
5✔
154
      return match.captured(1);
1✔
155
    }
156
  }
5✔
157
  for (const QString &line : lines) {
7✔
158
    const QRegularExpressionMatch match = KEY_IMPORTED_FALLBACK.match(line);
4✔
159
    if (match.hasMatch()) {
4✔
160
      return match.captured(1);
2✔
161
    }
162
  }
4✔
163
  return QString();
164
}
165

NEW
166
void ImportKeyDialog::showError(const QString &message) {
×
NEW
167
  QMessageBox::warning(this, tr("Import Key"), message);
×
NEW
168
}
×
169

NEW
170
void ImportKeyDialog::showSuccess(const QString &keyId) {
×
NEW
171
  QMessageBox::information(this, tr("Import Key"),
×
NEW
172
                           tr("Successfully imported key: %1").arg(keyId));
×
NEW
173
}
×
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