Page Menu
Home
GnuPG
Search
Configure Global Search
Log In
Files
F34311502
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Size
84 KB
Subscribers
None
View Options
diff --git a/src/commands/creategroupcommand.cpp b/src/commands/creategroupcommand.cpp
index d58ccf24c..34a4508ac 100644
--- a/src/commands/creategroupcommand.cpp
+++ b/src/commands/creategroupcommand.cpp
@@ -1,115 +1,116 @@
/*
This file is part of Kleopatra, the KDE keymanager
SPDX-FileCopyrightText: 2024 g10 Code GmbH
SPDX-FileContributor: Tobias Fella <tobias.fella@gnupg.com>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "creategroupcommand.h"
#include "command_p.h"
#include "dialogs/editgroupdialog.h"
+#include <Libkleo/Compat>
#include <Libkleo/KeyCache>
#include <gpgme++/key.h>
#include <KRandom>
#include <memory>
using namespace Kleo;
using namespace GpgME;
using namespace Kleo::Dialogs;
class CreateGroupCommand::Private : public Command::Private
{
friend class ::CreateGroupCommand;
CreateGroupCommand *q_func() const
{
return static_cast<CreateGroupCommand *>(q);
}
public:
using Command::Private::Private;
~Private() override;
KeyGroup showEditGroupDialog(const std::vector<Key> &keys, KeyGroup group, const QString &windowTitle, EditGroupDialog::FocusWidget focusWidget);
};
CreateGroupCommand::Private *CreateGroupCommand::d_func()
{
return static_cast<Private *>(d.get());
}
const CreateGroupCommand::Private *CreateGroupCommand::d_func() const
{
return static_cast<const Private *>(d.get());
}
#define d d_func()
#define q q_func()
CreateGroupCommand::Private::~Private() = default;
CreateGroupCommand::CreateGroupCommand(QAbstractItemView *v, KeyListController *p)
: Command(v, new Private(this, p))
{
}
CreateGroupCommand::~CreateGroupCommand() = default;
void CreateGroupCommand::doCancel()
{
}
KeyGroup CreateGroupCommand::Private::showEditGroupDialog(const std::vector<Key> &keys,
KeyGroup group,
const QString &windowTitle,
EditGroupDialog::FocusWidget focusWidget)
{
auto dialog = std::make_unique<EditGroupDialog>(parentWidgetOrView());
dialog->setWindowTitle(windowTitle);
dialog->setGroupName(group.name());
dialog->setInitialFocus(focusWidget);
dialog->setGroupKeys(keys);
const int result = dialog->exec();
if (result == QDialog::Rejected) {
return KeyGroup();
}
group.setName(dialog->groupName());
group.setKeys(dialog->groupKeys());
return group;
}
void CreateGroupCommand::doStart()
{
auto keys = d->keys();
auto removed = std::erase_if(keys, [](const auto &key) {
- return !key.hasEncrypt();
+ return !Kleo::keyHasEncrypt(key);
});
if (removed == d->keys().size()) {
KMessageBox::information(d->parentWidgetOrView(), i18n("None of the selected certificates can be used for encryption. No group will be created."));
return;
}
if (removed > 0) {
KMessageBox::information(d->parentWidgetOrView(),
i18n("Some of the selected certificates cannot be used for encryption. These will not be added to the group."));
}
const KeyGroup::Id newId = KRandom::randomString(8);
KeyGroup group = KeyGroup(newId, i18nc("default name for new group of keys", "New Group"), {}, KeyGroup::ApplicationConfig);
group.setIsImmutable(false);
const KeyGroup newGroup = d->showEditGroupDialog(keys, group, i18nc("@title:window a group of keys", "New Group"), EditGroupDialog::GroupName);
if (!newGroup.isNull()) {
auto groups = KeyCache::instance()->configurableGroups();
groups.push_back(newGroup);
KeyCache::mutableInstance()->saveConfigurableGroups(groups);
}
d->finished();
}
diff --git a/src/commands/importcertificatescommand.cpp b/src/commands/importcertificatescommand.cpp
index 4453823cb..042d60e7d 100644
--- a/src/commands/importcertificatescommand.cpp
+++ b/src/commands/importcertificatescommand.cpp
@@ -1,1090 +1,1091 @@
/* -*- mode: c++; c-basic-offset:4 -*-
commands/importcertificatescommand.cpp
This file is part of Kleopatra, the KDE keymanager
SPDX-FileCopyrightText: 2007, 2008 Klarälvdalens Datakonsult AB
SPDX-FileCopyrightText: 2016 Bundesamt für Sicherheit in der Informationstechnik
SPDX-FileContributor: Intevation GmbH
SPDX-FileCopyrightText: 2021, 2022 g10 Code GmbH
SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include <config-kleopatra.h>
#include "importcertificatescommand.h"
#include "importcertificatescommand_p.h"
#include "certifycertificatecommand.h"
#include "kleopatra_debug.h"
#include <settings.h>
#include <utils/memory-helpers.h>
#include <Libkleo/Algorithm>
+#include <Libkleo/Compat>
#include <Libkleo/Formatting>
#include <Libkleo/KeyCache>
#include <Libkleo/KeyGroupImportExport>
#include <Libkleo/KeyHelpers>
#include <Libkleo/KeyList>
#include <Libkleo/KeyListSortFilterProxyModel>
#include <Libkleo/MessageBox>
#include <Libkleo/Predicates>
#include <Libkleo/Stl_Util>
#include <QGpgME/ChangeOwnerTrustJob>
#include <QGpgME/ImportFromKeyserverJob>
#include <QGpgME/ImportJob>
#include <QGpgME/Protocol>
#include <QGpgME/ReceiveKeysJob>
#include <gpgme++/context.h>
#include <gpgme++/global.h>
#include <gpgme++/importresult.h>
#include <gpgme++/key.h>
#include <gpgme++/keylistresult.h>
#include <KLocalizedString>
#include <KMessageBox>
#include <QByteArray>
#include <QEventLoop>
#include <QProgressDialog>
#include <QString>
#include <QTextDocument> // for Qt::escape
#include <QTreeView>
#include <QWidget>
#include <algorithm>
#include <map>
#include <memory>
#include <set>
#include <unordered_set>
using namespace GpgME;
using namespace Kleo;
using namespace QGpgME;
static void disconnectConnection(const QMetaObject::Connection &connection)
{
// trivial function for disconnecting a signal-slot connection
QObject::disconnect(connection);
}
bool operator==(const ImportJobData &lhs, const ImportJobData &rhs)
{
return lhs.job == rhs.job;
}
namespace
{
make_comparator_str(ByImportFingerprint, .fingerprint());
class ImportResultProxyModel : public AbstractKeyListSortFilterProxyModel
{
Q_OBJECT
public:
ImportResultProxyModel(const std::vector<ImportResultData> &results, QObject *parent = nullptr)
: AbstractKeyListSortFilterProxyModel(parent)
{
updateFindCache(results);
}
~ImportResultProxyModel() override
{
}
ImportResultProxyModel *clone() const override
{
// compiler-generated copy ctor is fine!
return new ImportResultProxyModel(*this);
}
void setImportResults(const std::vector<ImportResultData> &results)
{
updateFindCache(results);
invalidateFilter();
}
protected:
QVariant data(const QModelIndex &index, int role) const override
{
if (!index.isValid() || role != Qt::ToolTipRole) {
return AbstractKeyListSortFilterProxyModel::data(index, role);
}
const QString fpr = index.data(KeyList::FingerprintRole).toString();
// find information:
const std::vector<Import>::const_iterator it =
Kleo::binary_find(m_importsByFingerprint.begin(), m_importsByFingerprint.end(), fpr.toLatin1().constData(), ByImportFingerprint<std::less>());
if (it == m_importsByFingerprint.end()) {
return AbstractKeyListSortFilterProxyModel::data(index, role);
} else {
QStringList rv;
const auto ids = m_idsByFingerprint[it->fingerprint()];
rv.reserve(ids.size());
std::copy(ids.cbegin(), ids.cend(), std::back_inserter(rv));
return Formatting::importMetaData(*it, rv);
}
}
bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override
{
//
// 0. Keep parents of matching children:
//
const QModelIndex index = sourceModel()->index(source_row, 0, source_parent);
Q_ASSERT(index.isValid());
for (int i = 0, end = sourceModel()->rowCount(index); i != end; ++i)
if (filterAcceptsRow(i, index)) {
return true;
}
//
// 1. Check that this is an imported key:
//
const QString fpr = index.data(KeyList::FingerprintRole).toString();
return std::binary_search(m_importsByFingerprint.begin(), m_importsByFingerprint.end(), fpr.toLatin1().constData(), ByImportFingerprint<std::less>());
}
private:
void updateFindCache(const std::vector<ImportResultData> &results)
{
m_importsByFingerprint.clear();
m_idsByFingerprint.clear();
m_results = results;
for (const auto &r : results) {
const std::vector<Import> imports = r.result.imports();
m_importsByFingerprint.insert(m_importsByFingerprint.end(), imports.begin(), imports.end());
for (std::vector<Import>::const_iterator it = imports.begin(), end = imports.end(); it != end; ++it) {
m_idsByFingerprint[it->fingerprint()].insert(r.id);
}
}
std::sort(m_importsByFingerprint.begin(), m_importsByFingerprint.end(), ByImportFingerprint<std::less>());
}
private:
mutable std::vector<Import> m_importsByFingerprint;
mutable std::map<const char *, std::set<QString>, ByImportFingerprint<std::less>> m_idsByFingerprint;
std::vector<ImportResultData> m_results;
};
bool importFailed(const ImportResultData &r)
{
// ignore GPG_ERR_EOF error to handle the "failed" import of files
// without X.509 certificates by gpgsm gracefully
return r.result.error() && r.result.error().code() != GPG_ERR_EOF;
}
bool importWasCanceled(const ImportResultData &r)
{
return r.result.error().isCanceled();
}
}
ImportCertificatesCommand::Private::Private(ImportCertificatesCommand *qq, KeyListController *c)
: Command::Private(qq, c)
, progressWindowTitle{i18nc("@title:window", "Importing Certificates")}
, progressLabelText{i18n("Importing certificates... (this can take a while)")}
{
}
ImportCertificatesCommand::Private::~Private()
{
if (progressDialog) {
delete progressDialog;
}
}
#define d d_func()
#define q q_func()
ImportCertificatesCommand::ImportCertificatesCommand(KeyListController *p)
: Command(new Private(this, p))
{
}
ImportCertificatesCommand::ImportCertificatesCommand(QAbstractItemView *v, KeyListController *p)
: Command(v, new Private(this, p))
{
}
ImportCertificatesCommand::~ImportCertificatesCommand() = default;
static QString format_ids(const std::vector<QString> &ids)
{
QStringList escapedIds;
for (const QString &id : ids) {
if (!id.isEmpty()) {
escapedIds << id.toHtmlEscaped();
}
}
return escapedIds.join(QLatin1String("<br>"));
}
static QString make_tooltip(const std::vector<ImportResultData> &results)
{
if (results.empty()) {
return {};
}
std::vector<QString> ids;
ids.reserve(results.size());
std::transform(std::begin(results), std::end(results), std::back_inserter(ids), [](const auto &r) {
return r.id;
});
std::sort(std::begin(ids), std::end(ids));
ids.erase(std::unique(std::begin(ids), std::end(ids)), std::end(ids));
if (ids.size() == 1)
if (ids.front().isEmpty()) {
return {};
} else
return i18nc("@info:tooltip", "Imported Certificates from %1", ids.front().toHtmlEscaped());
else
return i18nc("@info:tooltip", "Imported certificates from these sources:<br/>%1", format_ids(ids));
}
void ImportCertificatesCommand::Private::setImportResultProxyModel(const std::vector<ImportResultData> &results)
{
if (std::none_of(std::begin(results), std::end(results), [](const auto &r) {
return r.result.numConsidered() > 0;
})) {
return;
}
q->addTemporaryView(i18nc("@title:tab", "Imported Certificates"), new ImportResultProxyModel(results), make_tooltip(results));
if (QTreeView *const tv = qobject_cast<QTreeView *>(parentWidgetOrView())) {
tv->expandAll();
}
}
int sum(const std::vector<ImportResult> &res, int (ImportResult::*fun)() const)
{
return kdtools::accumulate_transform(res.begin(), res.end(), std::mem_fn(fun), 0);
}
static QString make_report(const std::vector<ImportResultData> &results, const std::vector<ImportedGroup> &groups)
{
const KLocalizedString normalLine = ki18n("<tr><td align=\"right\">%1</td><td>%2</td></tr>");
const KLocalizedString boldLine = ki18n("<tr><td align=\"right\"><b>%1</b></td><td>%2</td></tr>");
const KLocalizedString headerLine = ki18n("<tr><th colspan=\"2\" align=\"center\">%1</th></tr>");
std::vector<ImportResult> res;
res.reserve(results.size());
std::transform(std::begin(results), std::end(results), std::back_inserter(res), [](const auto &r) {
return r.result;
});
const auto numProcessedCertificates = sum(res, &ImportResult::numConsidered);
QStringList lines;
if (numProcessedCertificates > 0 || groups.size() == 0) {
lines.push_back(headerLine.subs(i18n("Certificates")).toString());
lines.push_back(normalLine.subs(i18n("Total number processed:")).subs(numProcessedCertificates).toString());
lines.push_back(normalLine.subs(i18n("Imported:")).subs(sum(res, &ImportResult::numImported)).toString());
if (const int n = sum(res, &ImportResult::newSignatures))
lines.push_back(normalLine.subs(i18n("New signatures:")).subs(n).toString());
if (const int n = sum(res, &ImportResult::newUserIDs))
lines.push_back(normalLine.subs(i18n("New user IDs:")).subs(n).toString());
if (const int n = sum(res, &ImportResult::numKeysWithoutUserID))
lines.push_back(normalLine.subs(i18n("Certificates without user IDs:")).subs(n).toString());
if (const int n = sum(res, &ImportResult::newSubkeys))
lines.push_back(normalLine.subs(i18n("New subkeys:")).subs(n).toString());
if (const int n = sum(res, &ImportResult::newRevocations))
lines.push_back(boldLine.subs(i18n("Newly revoked:")).subs(n).toString());
if (const int n = sum(res, &ImportResult::notImported))
lines.push_back(boldLine.subs(i18n("Not imported:")).subs(n).toString());
if (const int n = sum(res, &ImportResult::numUnchanged))
lines.push_back(normalLine.subs(i18n("Unchanged:")).subs(n).toString());
if (const int n = sum(res, &ImportResult::numSecretKeysConsidered))
lines.push_back(normalLine.subs(i18n("Secret keys processed:")).subs(n).toString());
if (const int n = sum(res, &ImportResult::numSecretKeysImported))
lines.push_back(normalLine.subs(i18n("Secret keys imported:")).subs(n).toString());
if (const int n = sum(res, &ImportResult::numSecretKeysConsidered) - sum(res, &ImportResult::numSecretKeysImported)
- sum(res, &ImportResult::numSecretKeysUnchanged))
if (n > 0)
lines.push_back(boldLine.subs(i18n("Secret keys <em>not</em> imported:")).subs(n).toString());
if (const int n = sum(res, &ImportResult::numSecretKeysUnchanged))
lines.push_back(normalLine.subs(i18n("Secret keys unchanged:")).subs(n).toString());
if (const int n = sum(res, &ImportResult::numV3KeysSkipped))
lines.push_back(normalLine.subs(i18n("Deprecated PGP-2 keys skipped:")).subs(n).toString());
}
if (!lines.empty()) {
lines.push_back(headerLine.subs(QLatin1String{" "}).toString());
}
if (groups.size() > 0) {
const auto newGroups = std::count_if(std::begin(groups), std::end(groups), [](const auto &g) {
return g.status == ImportedGroup::Status::New;
});
const auto updatedGroups = groups.size() - newGroups;
lines.push_back(headerLine.subs(i18n("Certificate Groups")).toString());
lines.push_back(normalLine.subs(i18n("Total number processed:")).subs(groups.size()).toString());
lines.push_back(normalLine.subs(i18n("New groups:")).subs(newGroups).toString());
lines.push_back(normalLine.subs(i18n("Updated groups:")).subs(updatedGroups).toString());
}
return lines.join(QLatin1String{});
}
static bool isImportFromSingleSource(const std::vector<ImportResultData> &res)
{
return (res.size() == 1) || (res.size() == 2 && res[0].id == res[1].id);
}
static QString make_message_report(const std::vector<ImportResultData> &res, const std::vector<ImportedGroup> &groups)
{
QString report{QLatin1String{"<html>"}};
if (res.empty()) {
report += i18n("No imports (should not happen, please report a bug).");
} else {
const QString title = isImportFromSingleSource(res) && !res.front().id.isEmpty() ? i18n("Detailed results of importing %1:", res.front().id)
: i18n("Detailed results of import:");
report += QLatin1String{"<p>"} + title + QLatin1String{"</p>"};
report += QLatin1String{"<p><table width=\"100%\">"};
report += make_report(res, groups);
report += QLatin1String{"</table></p>"};
}
report += QLatin1String{"</html>"};
return report;
}
// Returns false on error, true if please certify was shown.
bool ImportCertificatesCommand::Private::showPleaseCertify(const GpgME::Import &imp)
{
if (!Kleo::userHasCertificationKey()) {
qCDebug(KLEOPATRA_LOG) << q << __func__ << "No certification key available";
return false;
}
const char *fpr = imp.fingerprint();
if (!fpr) {
// WTF
qCWarning(KLEOPATRA_LOG) << "Import without fingerprint";
return false;
}
// Exactly one public key imported. Let's see if it is openpgp. We are async here so
// we can just fetch it.
auto ctx = wrap_unique(GpgME::Context::createForProtocol(GpgME::OpenPGP));
if (!ctx) {
// WTF
qCWarning(KLEOPATRA_LOG) << "Failed to create OpenPGP proto";
return false;
}
ctx->addKeyListMode(KeyListMode::WithSecret);
GpgME::Error err;
const auto key = ctx->key(fpr, err, false);
if (key.isNull() || err) {
// No such key most likely not OpenPGP
return false;
}
if (!Kleo::canBeCertified(key)) {
// key is expired or revoked
return false;
}
if (key.hasSecret()) {
qCDebug(KLEOPATRA_LOG) << q << __func__ << "Secret key is available -> skipping certification";
return false;
}
for (const auto &uid : key.userIDs()) {
if (uid.validity() >= GpgME::UserID::Marginal) {
// Already marginal so don't bug the user
return false;
}
}
const QStringList suggestions = {
i18n("A phone call to the person."),
i18n("Using a business card."),
i18n("Confirming it on a trusted website."),
};
auto sel = KMessageBox::questionTwoActions(parentWidgetOrView(),
i18n("In order to mark the certificate as valid it needs to be certified.") + QStringLiteral("<br>")
+ i18n("Certifying means that you check the Fingerprint.") + QStringLiteral("<br>")
+ i18n("Some suggestions to do this are:")
+ QStringLiteral("<li><ul>%1").arg(suggestions.join(QStringLiteral("</ul><ul>")))
+ QStringLiteral("</ul></li>") + i18n("Do you wish to start this process now?"),
i18nc("@title", "You have imported a new certificate (public key)"),
KGuiItem(i18nc("@action:button", "Certify")),
KStandardGuiItem::cancel(),
QStringLiteral("CertifyQuestion"));
if (sel == KMessageBox::ButtonCode::PrimaryAction) {
QEventLoop loop;
auto cmd = new Commands::CertifyCertificateCommand(key);
cmd->setParentWidget(parentWidgetOrView());
connect(cmd, &Command::finished, &loop, &QEventLoop::quit);
QMetaObject::invokeMethod(cmd, &Commands::CertifyCertificateCommand::start, Qt::QueuedConnection);
loop.exec();
}
return true;
}
namespace
{
/**
* Returns the Import of an OpenPGP key, if a single certificate was imported and this was an OpenPGP key.
* Otherwise, returns a null Import.
*/
auto getSingleOpenPGPImport(const std::vector<ImportResultData> &res)
{
static const Import nullImport;
if (!isImportFromSingleSource(res)) {
return nullImport;
}
const auto numImported = std::accumulate(res.cbegin(), res.cend(), 0, [](auto s, const auto &r) {
return s + r.result.numImported();
});
if (numImported > 1) {
return nullImport;
}
if ((res.size() >= 1) && (res[0].protocol == GpgME::OpenPGP) && (res[0].result.numImported() == 1) && (res[0].result.imports().size() == 1)) {
return res[0].result.imports()[0];
} else if ((res.size() == 2) && (res[1].protocol == GpgME::OpenPGP) && (res[1].result.numImported() == 1) && (res[1].result.imports().size() == 1)) {
return res[1].result.imports()[0];
}
return nullImport;
}
auto consolidatedAuditLogEntries(const std::vector<ImportResultData> &res)
{
static const QString gpg = QStringLiteral("gpg");
static const QString gpgsm = QStringLiteral("gpgsm");
if (res.size() == 1) {
return res.front().auditLog;
}
QStringList auditLogs;
auto extractAndAnnotateAuditLog = [](const ImportResultData &r) {
QString s;
if (!r.id.isEmpty()) {
const auto program = r.protocol == GpgME::OpenPGP ? gpg : gpgsm;
const auto headerLine = i18nc("file name (imported with gpg/gpgsm)", "%1 (imported with %2)").arg(r.id, program);
s += QStringLiteral("<div><b>%1</b></div>").arg(headerLine);
}
if (r.auditLog.error().code() == GPG_ERR_NO_DATA) {
s += QStringLiteral("<em>") + i18nc("@info", "Audit log is empty.") + QStringLiteral("</em>");
} else if (r.result.error().isCanceled()) {
s += QStringLiteral("<em>") + i18nc("@info", "Import was canceled.") + QStringLiteral("</em>");
} else {
s += r.auditLog.text();
}
return s;
};
std::transform(res.cbegin(), res.cend(), std::back_inserter(auditLogs), extractAndAnnotateAuditLog);
return AuditLogEntry{auditLogs.join(QLatin1String{"<hr>"}), Error{}};
}
}
void ImportCertificatesCommand::Private::showDetails(const std::vector<ImportResultData> &res, const std::vector<ImportedGroup> &groups)
{
const auto singleOpenPGPImport = getSingleOpenPGPImport(res);
if (!singleOpenPGPImport.isNull()) {
if (showPleaseCertify(singleOpenPGPImport)) {
return;
}
}
setImportResultProxyModel(res);
MessageBox::information(parentWidgetOrView(), make_message_report(res, groups), consolidatedAuditLogEntries(res), i18n("Certificate Import Result"));
}
static QString make_error_message(const Error &err, const QString &id)
{
Q_ASSERT(err);
Q_ASSERT(!err.isCanceled());
if (id.isEmpty()) {
return i18n(
"<qt><p>An error occurred while trying to import the certificate:</p>"
"<p><b>%1</b></p></qt>",
Formatting::errorAsString(err));
} else {
return i18n(
"<qt><p>An error occurred while trying to import the certificate %1:</p>"
"<p><b>%2</b></p></qt>",
id,
Formatting::errorAsString(err));
}
}
void ImportCertificatesCommand::Private::showError(const ImportResultData &result)
{
MessageBox::error(parentWidgetOrView(), make_error_message(result.result.error(), result.id), result.auditLog);
}
void ImportCertificatesCommand::Private::setWaitForMoreJobs(bool wait)
{
if (wait == waitForMoreJobs) {
return;
}
waitForMoreJobs = wait;
if (!waitForMoreJobs) {
tryToFinish();
}
}
void ImportCertificatesCommand::Private::onImportResult(const ImportResult &result, QGpgME::Job *finishedJob)
{
if (!finishedJob) {
finishedJob = qobject_cast<QGpgME::Job *>(q->sender());
}
Q_ASSERT(finishedJob);
qCDebug(KLEOPATRA_LOG) << q << __func__ << finishedJob;
auto it = std::find_if(std::begin(runningJobs), std::end(runningJobs), [finishedJob](const auto &job) {
return job.job == finishedJob;
});
Q_ASSERT(it != std::end(runningJobs));
if (it == std::end(runningJobs)) {
qCWarning(KLEOPATRA_LOG) << __func__ << "Error: Finished job not found";
return;
}
Kleo::for_each(it->connections, &disconnectConnection);
it->connections.clear();
increaseProgressValue();
const auto job = *it;
addImportResult({job.id, job.protocol, job.type, result, AuditLogEntry::fromJob(finishedJob)}, job);
}
void ImportCertificatesCommand::Private::addImportResult(const ImportResultData &result, const ImportJobData &job)
{
qCDebug(KLEOPATRA_LOG) << q << __func__ << result.id << "Result:" << Formatting::errorAsString(result.result.error());
results.push_back(result);
if (importFailed(result)) {
showError(result);
}
if (job.job) {
const auto count = std::erase(runningJobs, job);
Q_ASSERT(count == 1);
}
tryToFinish();
}
static void handleOwnerTrust(const std::vector<ImportResultData> &results, QWidget *dialog)
{
std::unordered_set<std::string> askedAboutFingerprints;
for (const auto &r : results) {
if (r.protocol != GpgME::Protocol::OpenPGP) {
qCDebug(KLEOPATRA_LOG) << __func__ << "Skipping non-OpenPGP import";
continue;
}
const auto imports = r.result.imports();
for (const auto &import : imports) {
if (!(import.status() & (Import::Status::NewKey | Import::Status::ContainedSecretKey))) {
qCDebug(KLEOPATRA_LOG) << __func__ << "Skipping already known imported public key";
continue;
}
const char *fpr = import.fingerprint();
if (!fpr) {
qCDebug(KLEOPATRA_LOG) << __func__ << "Skipping import without fingerprint";
continue;
}
if (Kleo::contains(askedAboutFingerprints, fpr)) {
// imports of secret keys can result in multiple Imports for the same key
qCDebug(KLEOPATRA_LOG) << __func__ << "Skipping import for already handled fingerprint";
continue;
}
GpgME::Error err;
auto ctx = wrap_unique(Context::createForProtocol(GpgME::Protocol::OpenPGP));
if (!ctx) {
qCWarning(KLEOPATRA_LOG) << "Failed to get context";
continue;
}
ctx->addKeyListMode(KeyListMode::WithSecret);
const Key toTrustOwner = ctx->key(fpr, err, false);
if (toTrustOwner.isNull() || !toTrustOwner.hasSecret()) {
continue;
}
if (toTrustOwner.ownerTrust() == Key::OwnerTrust::Ultimate) {
qCDebug(KLEOPATRA_LOG) << __func__ << "Skipping key with ultimate ownertrust";
continue;
}
const auto toTrustOwnerUserIDs{toTrustOwner.userIDs()};
// ki18n(" ") as initializer because initializing with empty string leads to
// (I18N_EMPTY_MESSAGE)
const KLocalizedString uids = std::accumulate(toTrustOwnerUserIDs.cbegin(),
toTrustOwnerUserIDs.cend(),
KLocalizedString{ki18n(" ")},
[](KLocalizedString temp, const auto &uid) {
return kxi18nc("@info", "%1<item>%2</item>").subs(temp).subs(Formatting::prettyNameAndEMail(uid));
});
const QString str = xi18nc("@info",
"<para>You have imported a certificate with fingerprint</para>"
"<para><numid>%1</numid></para>"
"<para>"
"and user IDs"
"<list>%2</list>"
"</para>"
"<para>Is this your own certificate?</para>",
Formatting::prettyID(fpr),
uids);
int k = KMessageBox::questionTwoActionsCancel(dialog,
str,
i18nc("@title:window", "Mark Own Certificate"),
KGuiItem{i18nc("@action:button", "Yes, It's Mine")},
KGuiItem{i18nc("@action:button", "No, It's Not Mine")});
askedAboutFingerprints.insert(fpr);
if (k == KMessageBox::ButtonCode::PrimaryAction) {
// To use the ChangeOwnerTrustJob over
// the CryptoBackendFactory
const QGpgME::Protocol *const backend = QGpgME::openpgp();
if (!backend) {
qCWarning(KLEOPATRA_LOG) << "Failed to get CryptoBackend";
return;
}
ChangeOwnerTrustJob *const j = backend->changeOwnerTrustJob();
j->start(toTrustOwner, Key::Ultimate);
} else if (k == KMessageBox::ButtonCode::Cancel) {
// do not bother the user with further "Is this yours?" questions
return;
}
}
}
}
static void validateImportedCertificate(const GpgME::Import &import)
{
if (const auto fpr = import.fingerprint()) {
auto key = KeyCache::instance()->findByFingerprint(fpr);
if (!key.isNull()) {
// this triggers a keylisting with validation for this certificate
key.update();
} else {
qCWarning(KLEOPATRA_LOG) << __func__ << "Certificate with fingerprint" << fpr << "not found";
}
}
}
static void handleExternalCMSImports(const std::vector<ImportResultData> &results)
{
// For external CMS Imports we have to manually do a keylist
// with validation to get the intermediate and root ca imported
// automatically if trusted-certs and extra-certs are used.
for (const auto &r : results) {
if (r.protocol == GpgME::CMS && r.type == ImportType::External && !importFailed(r) && !importWasCanceled(r)) {
const auto imports = r.result.imports();
std::for_each(std::begin(imports), std::end(imports), &validateImportedCertificate);
}
}
}
void ImportCertificatesCommand::Private::processResults()
{
importGroups();
if (Settings{}.retrieveSignerKeysAfterImport() && !importingSignerKeys) {
importingSignerKeys = true;
const auto missingSignerKeys = getMissingSignerKeyIds(results);
if (!missingSignerKeys.empty()) {
importSignerKeys(missingSignerKeys);
return;
}
}
handleExternalCMSImports(results);
// ensure that the progress dialog is closed before we show any other dialogs
setProgressToMaximum();
handleOwnerTrust(results, parentWidgetOrView());
showDetails(results, importedGroups);
auto tv = dynamic_cast<QTreeView *>(view());
if (!tv) {
qCDebug(KLEOPATRA_LOG) << "Failed to find treeview";
} else {
tv->expandAll();
}
finished();
}
void ImportCertificatesCommand::Private::tryToFinish()
{
qCDebug(KLEOPATRA_LOG) << q << __func__;
if (waitForMoreJobs) {
qCDebug(KLEOPATRA_LOG) << q << __func__ << "Waiting for more jobs -> keep going";
return;
}
if (!runningJobs.empty()) {
qCDebug(KLEOPATRA_LOG) << q << __func__ << "There are unfinished jobs -> keep going";
return;
}
if (!pendingJobs.empty()) {
qCDebug(KLEOPATRA_LOG) << q << __func__ << "There are pending jobs -> start the next one";
auto job = pendingJobs.front();
pendingJobs.pop();
job.job->startNow();
runningJobs.push_back(job);
return;
}
if (keyListConnection) {
qCWarning(KLEOPATRA_LOG) << q << __func__ << "There is already a valid keyListConnection!";
} else {
auto keyCache = KeyCache::mutableInstance();
keyListConnection = connect(keyCache.get(), &KeyCache::keyListingDone, q, [this]() {
keyCacheUpdated();
});
keyCache->startKeyListing();
}
}
void ImportCertificatesCommand::Private::keyCacheUpdated()
{
qCDebug(KLEOPATRA_LOG) << q << __func__;
if (!disconnect(keyListConnection)) {
qCWarning(KLEOPATRA_LOG) << q << __func__ << "Failed to disconnect keyListConnection";
}
keyCacheAutoRefreshSuspension.reset();
const auto allIds = std::accumulate(std::cbegin(results), std::cend(results), std::set<QString>{}, [](auto allIds, const auto &r) {
allIds.insert(r.id);
return allIds;
});
const auto canceledIds = std::accumulate(std::cbegin(results), std::cend(results), std::set<QString>{}, [](auto canceledIds, const auto &r) {
if (importWasCanceled(r)) {
canceledIds.insert(r.id);
}
return canceledIds;
});
const auto totalConsidered = std::accumulate(std::cbegin(results), std::cend(results), 0, [](auto totalConsidered, const auto &r) {
return totalConsidered + r.result.numConsidered();
});
if (totalConsidered == 0 && canceledIds.size() == allIds.size()) {
// nothing was considered for import and at least one import per id was
// canceled => treat the command as canceled
canceled();
return;
}
processResults();
}
static ImportedGroup storeGroup(const KeyGroup &group, const QString &id, QWidget *parent)
{
if (Kleo::any_of(group.keys(), [](const auto &key) {
- return !key.hasEncrypt();
+ return !Kleo::keyHasEncrypt(key);
})) {
KMessageBox::information(parent,
xi18nc("@info",
"<para>The imported group</para><para><emphasis>%1</emphasis></para><para>contains certificates that cannot be used for encryption. "
"This may lead to unexpected results.</para>",
group.name()));
}
const auto status = KeyCache::instance()->group(group.id()).isNull() ? ImportedGroup::Status::New : ImportedGroup::Status::Updated;
if (status == ImportedGroup::Status::New) {
KeyCache::mutableInstance()->insert(group);
} else {
KeyCache::mutableInstance()->update(group);
}
return {id, group, status};
}
void ImportCertificatesCommand::Private::importGroups()
{
for (const auto &path : filesToImportGroupsFrom) {
const bool certificateImportSucceeded = std::any_of(std::cbegin(results), std::cend(results), [path](const auto &r) {
return r.id == path && !importFailed(r) && !importWasCanceled(r);
});
if (certificateImportSucceeded) {
qCDebug(KLEOPATRA_LOG) << __func__ << "Importing groups from file" << path;
const auto groups = readKeyGroups(path);
std::transform(std::begin(groups), std::end(groups), std::back_inserter(importedGroups), [path, this](const auto &group) {
return storeGroup(group, path, parentWidgetOrView());
});
}
increaseProgressValue();
}
filesToImportGroupsFrom.clear();
}
static auto accumulateNewKeys(std::vector<std::string> &fingerprints, const std::vector<GpgME::Import> &imports)
{
return std::accumulate(std::begin(imports), std::end(imports), fingerprints, [](auto fingerprints, const auto &import) {
if (import.status() == Import::NewKey) {
fingerprints.push_back(import.fingerprint());
}
return fingerprints;
});
}
static auto accumulateNewOpenPGPKeys(const std::vector<ImportResultData> &results)
{
return std::accumulate(std::begin(results), std::end(results), std::vector<std::string>{}, [](auto fingerprints, const auto &r) {
if (r.protocol == GpgME::OpenPGP) {
fingerprints = accumulateNewKeys(fingerprints, r.result.imports());
}
return fingerprints;
});
}
std::set<QString> ImportCertificatesCommand::Private::getMissingSignerKeyIds(const std::vector<ImportResultData> &results)
{
auto newOpenPGPKeys = KeyCache::instance()->findByFingerprint(accumulateNewOpenPGPKeys(results));
// update all new OpenPGP keys to get information about certifications
std::for_each(std::begin(newOpenPGPKeys), std::end(newOpenPGPKeys), std::mem_fn(&Key::update));
auto missingSignerKeyIds = Kleo::getMissingSignerKeyIds(newOpenPGPKeys);
return missingSignerKeyIds;
}
void ImportCertificatesCommand::Private::importSignerKeys(const std::set<QString> &keyIds)
{
Q_ASSERT(!keyIds.empty());
setProgressLabelText(i18np("Fetching 1 signer key... (this can take a while)", "Fetching %1 signer keys... (this can take a while)", keyIds.size()));
setWaitForMoreJobs(true);
// start one import per key id to allow canceling the key retrieval without
// losing already retrieved keys
for (const auto &keyId : keyIds) {
startImport(GpgME::OpenPGP, {keyId}, QStringLiteral("Retrieve Signer Keys"));
}
setWaitForMoreJobs(false);
}
static std::unique_ptr<ImportJob> get_import_job(GpgME::Protocol protocol)
{
Q_ASSERT(protocol != UnknownProtocol);
if (const auto backend = (protocol == GpgME::OpenPGP ? QGpgME::openpgp() : QGpgME::smime())) {
return std::unique_ptr<ImportJob>(backend->importJob());
} else {
return std::unique_ptr<ImportJob>();
}
}
void ImportCertificatesCommand::Private::startImport(GpgME::Protocol protocol,
const QByteArray &data,
const QString &id,
[[maybe_unused]] const ImportOptions &options)
{
Q_ASSERT(protocol != UnknownProtocol);
if (std::find(nonWorkingProtocols.cbegin(), nonWorkingProtocols.cend(), protocol) != nonWorkingProtocols.cend()) {
return;
}
std::unique_ptr<ImportJob> job = get_import_job(protocol);
if (!job.get()) {
nonWorkingProtocols.push_back(protocol);
error(i18n("The type of this certificate (%1) is not supported by this Kleopatra installation.", Formatting::displayName(protocol)),
i18n("Certificate Import Failed"));
addImportResult({id, protocol, ImportType::Local, ImportResult{}, AuditLogEntry{}});
return;
}
keyCacheAutoRefreshSuspension = KeyCache::mutableInstance()->suspendAutoRefresh();
std::vector<QMetaObject::Connection> connections = {
connect(job.get(),
&AbstractImportJob::result,
q,
[this](const GpgME::ImportResult &result) {
onImportResult(result);
}),
connect(job.get(), &QGpgME::Job::jobProgress, q, &Command::progress),
};
job->setImportFilter(options.importFilter);
job->setKeyOrigin(options.keyOrigin, options.keyOriginUrl);
const GpgME::Error err = job->startLater(data);
if (err.code()) {
addImportResult({id, protocol, ImportType::Local, ImportResult{err}, AuditLogEntry{}});
} else {
increaseProgressMaximum();
pendingJobs.push({id, protocol, ImportType::Local, job.release(), connections});
}
}
static std::unique_ptr<ImportFromKeyserverJob> get_import_from_keyserver_job(GpgME::Protocol protocol)
{
Q_ASSERT(protocol != UnknownProtocol);
if (const auto backend = (protocol == GpgME::OpenPGP ? QGpgME::openpgp() : QGpgME::smime())) {
return std::unique_ptr<ImportFromKeyserverJob>(backend->importFromKeyserverJob());
} else {
return std::unique_ptr<ImportFromKeyserverJob>();
}
}
void ImportCertificatesCommand::Private::startImport(GpgME::Protocol protocol, const std::vector<Key> &keys, const QString &id)
{
Q_ASSERT(protocol != UnknownProtocol);
if (std::find(nonWorkingProtocols.cbegin(), nonWorkingProtocols.cend(), protocol) != nonWorkingProtocols.cend()) {
return;
}
std::unique_ptr<ImportFromKeyserverJob> job = get_import_from_keyserver_job(protocol);
if (!job.get()) {
nonWorkingProtocols.push_back(protocol);
error(i18n("The type of this certificate (%1) is not supported by this Kleopatra installation.", Formatting::displayName(protocol)),
i18n("Certificate Import Failed"));
addImportResult({id, protocol, ImportType::External, ImportResult{}, AuditLogEntry{}});
return;
}
keyCacheAutoRefreshSuspension = KeyCache::mutableInstance()->suspendAutoRefresh();
std::vector<QMetaObject::Connection> connections = {
connect(job.get(),
&AbstractImportJob::result,
q,
[this](const GpgME::ImportResult &result) {
onImportResult(result);
}),
connect(job.get(), &QGpgME::Job::jobProgress, q, &Command::progress),
};
const GpgME::Error err = job->start(keys);
if (err.code()) {
addImportResult({id, protocol, ImportType::External, ImportResult{err}, AuditLogEntry{}});
} else {
increaseProgressMaximum();
runningJobs.push_back({id, protocol, ImportType::External, job.release(), connections});
}
}
static auto get_receive_keys_job(GpgME::Protocol protocol)
{
Q_ASSERT(protocol != UnknownProtocol);
std::unique_ptr<ReceiveKeysJob> job{};
if (const auto backend = (protocol == GpgME::OpenPGP ? QGpgME::openpgp() : QGpgME::smime())) {
job.reset(backend->receiveKeysJob());
}
return job;
}
void ImportCertificatesCommand::Private::startImport(GpgME::Protocol protocol, [[maybe_unused]] const QStringList &keyIds, const QString &id)
{
Q_ASSERT(protocol != UnknownProtocol);
auto job = get_receive_keys_job(protocol);
if (!job.get()) {
qCWarning(KLEOPATRA_LOG) << "Failed to get ReceiveKeysJob for protocol" << Formatting::displayName(protocol);
addImportResult({id, protocol, ImportType::External, ImportResult{}, AuditLogEntry{}});
return;
}
keyCacheAutoRefreshSuspension = KeyCache::mutableInstance()->suspendAutoRefresh();
std::vector<QMetaObject::Connection> connections = {
connect(job.get(),
&AbstractImportJob::result,
q,
[this](const GpgME::ImportResult &result) {
onImportResult(result);
}),
connect(job.get(), &QGpgME::Job::jobProgress, q, &Command::progress),
};
const GpgME::Error err = job->start(keyIds);
if (err.code()) {
addImportResult({id, protocol, ImportType::External, ImportResult{err}, AuditLogEntry{}});
} else {
increaseProgressMaximum();
runningJobs.push_back({id, protocol, ImportType::External, job.release(), connections});
}
}
void ImportCertificatesCommand::Private::importGroupsFromFile(const QString &filename)
{
increaseProgressMaximum();
filesToImportGroupsFrom.push_back(filename);
}
void ImportCertificatesCommand::Private::setUpProgressDialog()
{
if (progressDialog) {
return;
}
progressDialog = new QProgressDialog{parentWidgetOrView()};
// use a non-modal progress dialog to avoid reentrancy problems (and crashes) if multiple jobs finish in the same event loop cycle
// (cf. the warning for QProgressDialog::setValue() in the API documentation)
progressDialog->setModal(false);
progressDialog->setWindowTitle(progressWindowTitle);
progressDialog->setLabelText(progressLabelText);
progressDialog->setMinimumDuration(1000);
progressDialog->setMaximum(1);
progressDialog->setValue(0);
connect(progressDialog, &QProgressDialog::canceled, q, &Command::cancel);
connect(q, &Command::finished, progressDialog, [this]() {
progressDialog->accept();
});
}
void ImportCertificatesCommand::Private::setProgressWindowTitle(const QString &title)
{
if (progressDialog) {
progressDialog->setWindowTitle(title);
} else {
progressWindowTitle = title;
}
}
void ImportCertificatesCommand::Private::setProgressLabelText(const QString &text)
{
if (progressDialog) {
progressDialog->setLabelText(text);
} else {
progressLabelText = text;
}
}
void ImportCertificatesCommand::Private::increaseProgressMaximum()
{
setUpProgressDialog();
progressDialog->setMaximum(progressDialog->maximum() + 1);
qCDebug(KLEOPATRA_LOG) << __func__ << "progress:" << progressDialog->value() << "/" << progressDialog->maximum();
}
void ImportCertificatesCommand::Private::increaseProgressValue()
{
progressDialog->setValue(progressDialog->value() + 1);
qCDebug(KLEOPATRA_LOG) << __func__ << "progress:" << progressDialog->value() << "/" << progressDialog->maximum();
}
void ImportCertificatesCommand::Private::setProgressToMaximum()
{
qCDebug(KLEOPATRA_LOG) << __func__;
progressDialog->setValue(progressDialog->maximum());
}
void ImportCertificatesCommand::doCancel()
{
const auto jobsToCancel = d->runningJobs;
std::for_each(std::begin(jobsToCancel), std::end(jobsToCancel), [this](const auto &job) {
if (!job.connections.empty()) {
// ignore jobs without connections; they are already completed
qCDebug(KLEOPATRA_LOG) << "Canceling job" << job.job;
job.job->slotCancel();
d->onImportResult(ImportResult{Error::fromCode(GPG_ERR_CANCELED)}, job.job);
}
});
}
#undef d
#undef q
#include "importcertificatescommand.moc"
#include "moc_importcertificatescommand.cpp"
diff --git a/src/conf/groupsconfigwidget.cpp b/src/conf/groupsconfigwidget.cpp
index b9cf35c2f..292d9550e 100644
--- a/src/conf/groupsconfigwidget.cpp
+++ b/src/conf/groupsconfigwidget.cpp
@@ -1,504 +1,505 @@
/*
conf/groupsconfigwidget.cpp
This file is part of Kleopatra, the KDE keymanager
SPDX-FileCopyrightText: 2021 g10 Code GmbH
SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "groupsconfigwidget.h"
#include <commands/certifygroupcommand.h>
#include <commands/exportgroupscommand.h>
#include <dialogs/editgroupdialog.h>
#include <Libkleo/Algorithm>
+#include <Libkleo/Compat>
#include <Libkleo/Debug>
#include <Libkleo/Formatting>
#include <Libkleo/KeyCache>
#include <Libkleo/KeyGroup>
#include <Libkleo/KeyHelpers>
#include <Libkleo/KeyListModel>
#include <Libkleo/KeyListSortFilterProxyModel>
#include <KLocalizedString>
#include <KMessageBox>
#include <KRandom>
#include <QItemSelectionModel>
#include <QLabel>
#include <QLineEdit>
#include <QListView>
#include <QPushButton>
#include <QVBoxLayout>
#include "kleopatra_debug.h"
using namespace Kleo;
using namespace Kleo::Dialogs;
Q_DECLARE_METATYPE(KeyGroup)
namespace
{
class ListView : public QListView
{
Q_OBJECT
public:
using QListView::QListView;
protected:
void currentChanged(const QModelIndex ¤t, const QModelIndex &previous) override
{
// workaround bug in QListView::currentChanged which sends an accessible focus event
// even if the list view doesn't have focus
if (hasFocus()) {
QListView::currentChanged(current, previous);
} else {
// skip the reimplementation of currentChanged in QListView
QAbstractItemView::currentChanged(current, previous);
}
}
void focusInEvent(QFocusEvent *event) override
{
QListView::focusInEvent(event);
// select current item if it isn't selected
if (currentIndex().isValid() && !selectionModel()->isSelected(currentIndex())) {
selectionModel()->select(currentIndex(), QItemSelectionModel::ClearAndSelect);
}
}
};
class ProxyModel : public AbstractKeyListSortFilterProxyModel
{
Q_OBJECT
public:
ProxyModel(QObject *parent = nullptr)
: AbstractKeyListSortFilterProxyModel(parent)
{
}
~ProxyModel() override = default;
ProxyModel *clone() const override
{
// compiler-generated copy ctor is fine!
return new ProxyModel(*this);
}
int columnCount(const QModelIndex &parent = {}) const override
{
Q_UNUSED(parent)
// pretend that there is only one column to workaround a bug in
// QAccessibleTable which provides the accessibility interface for the
// list view
return 1;
}
QVariant data(const QModelIndex &idx, int role) const override
{
if (!idx.isValid()) {
return {};
}
return AbstractKeyListSortFilterProxyModel::data(index(idx.row(), KeyList::Summary), role);
}
};
struct Selection {
KeyGroup current;
std::vector<KeyGroup> selected;
};
}
class GroupsConfigWidget::Private
{
friend class ::Kleo::GroupsConfigWidget;
GroupsConfigWidget *const q;
struct {
QLineEdit *groupsFilter = nullptr;
QListView *groupsList = nullptr;
QPushButton *newButton = nullptr;
QPushButton *editButton = nullptr;
QPushButton *deleteButton = nullptr;
QPushButton *certifyButton = nullptr;
QPushButton *exportButton = nullptr;
} ui;
AbstractKeyListModel *groupsModel = nullptr;
ProxyModel *groupsFilterModel = nullptr;
public:
Private(GroupsConfigWidget *qq)
: q(qq)
{
auto mainLayout = new QVBoxLayout(q);
auto groupsLayout = new QGridLayout;
groupsLayout->setContentsMargins(q->style()->pixelMetric(QStyle::PM_LayoutLeftMargin),
q->style()->pixelMetric(QStyle::PM_LayoutTopMargin),
q->style()->pixelMetric(QStyle::PM_LayoutRightMargin),
q->style()->pixelMetric(QStyle::PM_LayoutBottomMargin));
groupsLayout->setColumnStretch(0, 1);
groupsLayout->setRowStretch(1, 1);
int row = -1;
row++;
{
auto hbox = new QHBoxLayout;
auto label = new QLabel{i18nc("@label", "Search:")};
label->setAccessibleName(i18nc("@label", "Search groups"));
label->setToolTip(i18nc("@info:tooltip", "Search the list for groups matching the search term."));
hbox->addWidget(label);
ui.groupsFilter = new QLineEdit(q);
ui.groupsFilter->setClearButtonEnabled(true);
ui.groupsFilter->setAccessibleName(i18nc("@label", "Search groups"));
ui.groupsFilter->setToolTip(i18nc("@info:tooltip", "Search the list for groups matching the search term."));
ui.groupsFilter->setPlaceholderText(i18nc("@info::placeholder", "Enter search term"));
ui.groupsFilter->setCursorPosition(0); // prevent emission of accessible text cursor event before accessible focus event
label->setBuddy(ui.groupsFilter);
hbox->addWidget(ui.groupsFilter, 1);
groupsLayout->addLayout(hbox, row, 0);
}
row++;
groupsModel = AbstractKeyListModel::createFlatKeyListModel(q);
groupsFilterModel = new ProxyModel(q);
groupsFilterModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
groupsFilterModel->setFilterKeyColumn(KeyList::Summary);
groupsFilterModel->setSortCaseSensitivity(Qt::CaseInsensitive);
groupsFilterModel->setSourceModel(groupsModel);
groupsFilterModel->sort(KeyList::Summary, Qt::AscendingOrder);
ui.groupsList = new ListView(q);
ui.groupsList->setAccessibleName(i18nc("groups of keys", "groups"));
ui.groupsList->setModel(groupsFilterModel);
ui.groupsList->setSelectionBehavior(QAbstractItemView::SelectRows);
ui.groupsList->setSelectionMode(QAbstractItemView::ExtendedSelection);
groupsLayout->addWidget(ui.groupsList, row, 0);
auto groupsButtonLayout = new QVBoxLayout;
ui.newButton = new QPushButton(i18nc("@action:button", "New"), q);
groupsButtonLayout->addWidget(ui.newButton);
ui.editButton = new QPushButton(i18nc("@action:button", "Edit"), q);
ui.editButton->setEnabled(false);
groupsButtonLayout->addWidget(ui.editButton);
ui.deleteButton = new QPushButton(i18nc("@action:button", "Delete"), q);
ui.deleteButton->setEnabled(false);
groupsButtonLayout->addWidget(ui.deleteButton);
ui.certifyButton = new QPushButton{i18nc("@action:button", "Certify"), q};
ui.certifyButton->setToolTip(i18nc("@info:tooltip", "Start the certification process for all certificates in the group."));
ui.certifyButton->setEnabled(false);
groupsButtonLayout->addWidget(ui.certifyButton);
ui.exportButton = new QPushButton{i18nc("@action:button", "Export"), q};
ui.exportButton->setEnabled(false);
groupsButtonLayout->addWidget(ui.exportButton);
groupsButtonLayout->addStretch(1);
groupsLayout->addLayout(groupsButtonLayout, row, 1);
mainLayout->addLayout(groupsLayout, /*stretch=*/1);
connect(ui.groupsFilter, &QLineEdit::textChanged, q, [this](const auto &s) {
groupsFilterModel->setFilterRegularExpression(QRegularExpression::escape(s));
});
connect(ui.groupsList->selectionModel(), &QItemSelectionModel::selectionChanged, q, [this]() {
selectionChanged();
});
connect(ui.groupsList, &QListView::doubleClicked, q, [this](const QModelIndex &index) {
editGroup(index);
});
connect(ui.newButton, &QPushButton::clicked, q, [this]() {
addGroup();
});
connect(ui.editButton, &QPushButton::clicked, q, [this]() {
editGroup();
});
connect(ui.deleteButton, &QPushButton::clicked, q, [this]() {
deleteGroup();
});
connect(ui.certifyButton, &QPushButton::clicked, q, [this]() {
certifyGroup();
});
connect(ui.exportButton, &QPushButton::clicked, q, [this]() {
exportGroup();
});
}
private:
auto getGroupIndex(const KeyGroup &group)
{
QModelIndex index;
if (const KeyListModelInterface *const klmi = dynamic_cast<KeyListModelInterface *>(ui.groupsList->model())) {
index = klmi->index(group);
}
return index;
}
auto selectedRows()
{
return ui.groupsList->selectionModel()->selectedRows();
}
auto getGroup(const QModelIndex &index)
{
return index.isValid() ? ui.groupsList->model()->data(index, KeyList::GroupRole).value<KeyGroup>() : KeyGroup{};
}
auto getGroups(const QModelIndexList &indexes)
{
std::vector<KeyGroup> groups;
std::transform(std::begin(indexes), std::end(indexes), std::back_inserter(groups), [this](const auto &index) {
return getGroup(index);
});
return groups;
}
Selection saveSelection()
{
return {getGroup(ui.groupsList->selectionModel()->currentIndex()), getGroups(selectedRows())};
}
void restoreSelection(const Selection &selection)
{
auto selectionModel = ui.groupsList->selectionModel();
selectionModel->clearSelection();
for (const auto &group : selection.selected) {
selectionModel->select(getGroupIndex(group), QItemSelectionModel::Select | QItemSelectionModel::Rows);
}
auto currentIndex = getGroupIndex(selection.current);
if (currentIndex.isValid()) {
// keep current item if old current group is gone
selectionModel->setCurrentIndex(currentIndex, QItemSelectionModel::NoUpdate);
}
}
void selectionChanged()
{
const auto selectedGroups = getGroups(selectedRows());
const bool allSelectedGroupsAreEditable = std::all_of(std::begin(selectedGroups), std::end(selectedGroups), [](const auto &g) {
return !g.isNull() && !g.isImmutable();
});
ui.editButton->setEnabled(selectedGroups.size() == 1 && allSelectedGroupsAreEditable);
ui.deleteButton->setEnabled(!selectedGroups.empty() && allSelectedGroupsAreEditable);
ui.certifyButton->setEnabled(selectedGroups.size() == 1 //
&& !selectedGroups.front().keys().empty() //
&& allKeysHaveProtocol(selectedGroups.front().keys(), GpgME::OpenPGP));
ui.exportButton->setEnabled(selectedGroups.size() == 1);
}
KeyGroup showEditGroupDialog(KeyGroup group, const QString &windowTitle, EditGroupDialog::FocusWidget focusWidget)
{
auto dialog = std::make_unique<EditGroupDialog>(q);
dialog->setWindowTitle(windowTitle);
dialog->setGroupName(group.name());
const KeyGroup::Keys &keys = group.keys();
dialog->setGroupKeys(std::vector<GpgME::Key>(keys.cbegin(), keys.cend()));
dialog->setInitialFocus(focusWidget);
const int result = dialog->exec();
if (result == QDialog::Rejected) {
return KeyGroup();
}
group.setName(dialog->groupName());
group.setKeys(dialog->groupKeys());
return group;
}
void addGroup()
{
const KeyGroup::Id newId = KRandom::randomString(8);
KeyGroup group = KeyGroup(newId, i18nc("default name for new group of keys", "New Group"), {}, KeyGroup::ApplicationConfig);
group.setIsImmutable(false);
const KeyGroup newGroup = showEditGroupDialog(group, i18nc("@title:window a group of keys", "New Group"), EditGroupDialog::GroupName);
if (newGroup.isNull()) {
return;
}
const QModelIndex newIndex = groupsModel->addGroup(newGroup);
if (!newIndex.isValid()) {
qCDebug(KLEOPATRA_LOG) << "Adding group to model failed";
return;
}
Q_EMIT q->changed();
}
void editGroup(const QModelIndex &index = {})
{
QModelIndex groupIndex;
if (index.isValid()) {
groupIndex = index;
} else {
const auto selection = selectedRows();
if (selection.size() != 1) {
qCDebug(KLEOPATRA_LOG) << (selection.empty() ? "selection is empty" : "more than one group is selected");
return;
}
groupIndex = selection.front();
}
const KeyGroup group = getGroup(groupIndex);
if (group.isNull()) {
qCDebug(KLEOPATRA_LOG) << "selected group is null";
return;
}
if (group.isImmutable()) {
qCDebug(KLEOPATRA_LOG) << "selected group is immutable";
return;
}
const KeyGroup updatedGroup = showEditGroupDialog(group, i18nc("@title:window a group of keys", "Edit Group"), EditGroupDialog::KeysFilter);
if (updatedGroup.isNull()) {
return;
}
// look up index of updated group; the groupIndex used above may have become invalid
const auto updatedGroupIndex = getGroupIndex(updatedGroup);
if (updatedGroupIndex.isValid()) {
const bool success = ui.groupsList->model()->setData(updatedGroupIndex, QVariant::fromValue(updatedGroup));
if (!success) {
qCDebug(KLEOPATRA_LOG) << "Updating group in model failed";
return;
}
} else {
qCDebug(KLEOPATRA_LOG) << __func__ << "Failed to find index of group" << updatedGroup << "; maybe it was removed behind our back; re-add it";
const QModelIndex newIndex = groupsModel->addGroup(updatedGroup);
if (!newIndex.isValid()) {
qCDebug(KLEOPATRA_LOG) << "Re-adding group to model failed";
return;
}
}
Q_EMIT q->changed();
}
bool confirmDeletion(const std::vector<KeyGroup> &groups)
{
QString message;
QStringList groupSummaries;
if (groups.size() == 1) {
message = xi18nc("@info",
"<para>Do you really want to delete this group?</para>"
"<para><emphasis>%1</emphasis></para>"
"<para>Once deleted, it cannot be restored.</para>")
.arg(Formatting::summaryLine(groups.front()));
} else {
message = xi18ncp("@info",
"<para>Do you really want to delete this %1 group?</para>"
"<para>Once deleted, it cannot be restored.</para>",
"<para>Do you really want to delete these %1 groups?</para>"
"<para>Once deleted, they cannot be restored.</para>",
groups.size());
Kleo::transform(groups, std::back_inserter(groupSummaries), [](const auto &g) {
return Formatting::summaryLine(g);
});
}
const auto answer = KMessageBox::questionTwoActionsList(q,
message,
groupSummaries,
i18ncp("@title:window", "Delete Group", "Delete Groups", groups.size()),
KStandardGuiItem::del(),
KStandardGuiItem::cancel(),
{},
KMessageBox::Notify | KMessageBox::Dangerous);
return answer == KMessageBox::PrimaryAction;
}
void deleteGroup()
{
const auto selectedGroups = getGroups(selectedRows());
if (selectedGroups.empty()) {
qCDebug(KLEOPATRA_LOG) << "selection is empty";
return;
}
if (!confirmDeletion(selectedGroups)) {
return;
}
for (const auto &group : selectedGroups) {
const bool success = groupsModel->removeGroup(group);
if (!success) {
qCDebug(KLEOPATRA_LOG) << "Removing group from model failed:" << group;
}
}
Q_EMIT q->changed();
}
void certifyGroup()
{
const auto selectedGroups = getGroups(selectedRows());
if (selectedGroups.size() != 1) {
qCDebug(KLEOPATRA_LOG) << __func__ << (selectedGroups.empty() ? "selection is empty" : "more than one group is selected");
return;
}
auto cmd = new CertifyGroupCommand{selectedGroups.front()};
cmd->setParentWidget(q->window());
cmd->start();
}
void exportGroup()
{
const auto selectedGroups = getGroups(selectedRows());
if (selectedGroups.empty()) {
qCDebug(KLEOPATRA_LOG) << "selection is empty";
return;
}
if (Kleo::any_of(selectedGroups[0].keys(), [](const auto &key) {
- return !key.hasEncrypt();
+ return !Kleo::keyHasEncrypt(key);
})) {
KMessageBox::information(q->parentWidget(),
i18nc("@info", "The group contains keys that cannot be used for encryption. This may lead to unexpected results."));
}
auto cmd = new ExportGroupsCommand(selectedGroups);
cmd->start();
}
};
GroupsConfigWidget::GroupsConfigWidget(QWidget *parent)
: QWidget(parent)
, d(new Private(this))
{
}
GroupsConfigWidget::~GroupsConfigWidget() = default;
void GroupsConfigWidget::setGroups(const std::vector<KeyGroup> &groups)
{
const auto selection = d->saveSelection();
d->groupsModel->setGroups(groups);
d->restoreSelection(selection);
}
std::vector<KeyGroup> GroupsConfigWidget::groups() const
{
std::vector<KeyGroup> result;
result.reserve(d->groupsModel->rowCount());
for (int row = 0; row < d->groupsModel->rowCount(); ++row) {
const QModelIndex index = d->groupsModel->index(row, 0);
result.push_back(d->groupsModel->group(index));
}
return result;
}
#include "groupsconfigwidget.moc"
diff --git a/src/dialogs/editgroupdialog.cpp b/src/dialogs/editgroupdialog.cpp
index aece3f50a..049627ad9 100644
--- a/src/dialogs/editgroupdialog.cpp
+++ b/src/dialogs/editgroupdialog.cpp
@@ -1,485 +1,486 @@
/*
dialogs/editgroupdialog.cpp
This file is part of Kleopatra, the KDE keymanager
SPDX-FileCopyrightText: 2021 g10 Code GmbH
SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "editgroupdialog.h"
#include "commands/detailscommand.h"
#include "utils/gui-helper.h"
#include "view/keytreeview.h"
#include <settings.h>
#include <Libkleo/Algorithm>
+#include <Libkleo/Compat>
#include <Libkleo/DefaultKeyFilter>
#include <Libkleo/KeyCache>
#include <Libkleo/KeyListModel>
#include <Libkleo/KeyListSortFilterProxyModel>
#include <KColorScheme>
#include <KConfigGroup>
#include <KGuiItem>
#include <KLocalizedString>
#include <KSeparator>
#include <KSharedConfig>
#include <KStandardGuiItem>
#include <QApplication>
#include <QDialogButtonBox>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QItemSelectionModel>
#include <QLabel>
#include <QLineEdit>
#include <QPalette>
#include <QPushButton>
#include <QTreeView>
#include <QVBoxLayout>
#include "kleopatra_debug.h"
using namespace Kleo;
using namespace Kleo::Commands;
using namespace Kleo::Dialogs;
using namespace GpgME;
Q_DECLARE_METATYPE(GpgME::Key)
namespace
{
auto createOpenPGPOnlyKeyFilter()
{
auto filter = std::make_shared<DefaultKeyFilter>();
filter->setIsOpenPGP(DefaultKeyFilter::Set);
return filter;
}
}
class WarnNonEncryptionKeysProxyModel : public Kleo::AbstractKeyListSortFilterProxyModel
{
Q_OBJECT
public:
using Kleo::AbstractKeyListSortFilterProxyModel::AbstractKeyListSortFilterProxyModel;
~WarnNonEncryptionKeysProxyModel() override;
WarnNonEncryptionKeysProxyModel *clone() const override
{
return new WarnNonEncryptionKeysProxyModel(this->parent());
}
QVariant data(const QModelIndex &index, int role) const override
{
const auto sourceIndex = sourceModel()->index(index.row(), index.column());
- if (!sourceIndex.data(KeyList::KeyRole).value<Key>().hasEncrypt()) {
+ if (!Kleo::keyHasEncrypt(sourceIndex.data(KeyList::KeyRole).value<Key>())) {
if (role == Qt::DecorationRole && index.column() == 0) {
return QIcon::fromTheme(QStringLiteral("data-warning"));
}
if (role == Qt::ToolTipRole) {
return i18nc("@info:tooltip", "This certificate cannot be used for encryption.");
}
}
return sourceIndex.data(role);
}
};
WarnNonEncryptionKeysProxyModel::~WarnNonEncryptionKeysProxyModel() = default;
class DisableNonEncryptionKeysProxyModel : public Kleo::AbstractKeyListSortFilterProxyModel
{
Q_OBJECT
public:
using Kleo::AbstractKeyListSortFilterProxyModel::AbstractKeyListSortFilterProxyModel;
~DisableNonEncryptionKeysProxyModel() override;
DisableNonEncryptionKeysProxyModel *clone() const override
{
return new DisableNonEncryptionKeysProxyModel(this->parent());
}
QVariant data(const QModelIndex &index, int role) const override
{
const auto sourceIndex = sourceModel()->index(index.row(), index.column());
- if (!sourceIndex.data(KeyList::KeyRole).value<Key>().hasEncrypt()) {
+ if (!Kleo::keyHasEncrypt(sourceIndex.data(KeyList::KeyRole).value<Key>())) {
if (role == Qt::ForegroundRole) {
return qApp->palette().color(QPalette::Disabled, QPalette::Text);
}
if (role == Qt::BackgroundRole) {
return KColorScheme(QPalette::Disabled, KColorScheme::View).background(KColorScheme::NeutralBackground).color();
}
if (role == Qt::ToolTipRole) {
return i18nc("@info:tooltip", "This certificate cannot be added to the group as it cannot be used for encryption.");
}
}
return sourceIndex.data(role);
}
Qt::ItemFlags flags(const QModelIndex &index) const override
{
auto originalFlags = index.model()->QAbstractItemModel::flags(index);
- if (index.data(KeyList::KeyRole).value<Key>().hasEncrypt()) {
+ if (Kleo::keyHasEncrypt(index.data(KeyList::KeyRole).value<Key>())) {
return originalFlags;
} else {
return (originalFlags & ~Qt::ItemIsEnabled);
}
return {};
}
};
DisableNonEncryptionKeysProxyModel::~DisableNonEncryptionKeysProxyModel() = default;
class EditGroupDialog::Private
{
friend class ::Kleo::Dialogs::EditGroupDialog;
EditGroupDialog *const q;
struct {
QLineEdit *groupNameEdit = nullptr;
QLineEdit *availableKeysFilter = nullptr;
KeyTreeView *availableKeysList = nullptr;
QLineEdit *groupKeysFilter = nullptr;
KeyTreeView *groupKeysList = nullptr;
QDialogButtonBox *buttonBox = nullptr;
} ui;
AbstractKeyListModel *availableKeysModel = nullptr;
AbstractKeyListModel *groupKeysModel = nullptr;
public:
Private(EditGroupDialog *qq)
: q(qq)
{
auto mainLayout = new QVBoxLayout(q);
{
auto groupNameLayout = new QHBoxLayout();
auto label = new QLabel(i18nc("Name of a group of keys", "Name:"), q);
groupNameLayout->addWidget(label);
ui.groupNameEdit = new QLineEdit(q);
label->setBuddy(ui.groupNameEdit);
groupNameLayout->addWidget(ui.groupNameEdit);
mainLayout->addLayout(groupNameLayout);
}
mainLayout->addWidget(new KSeparator(Qt::Horizontal, q));
auto centerLayout = new QVBoxLayout;
auto availableKeysGroupBox = new QGroupBox{i18nc("@title", "Available Keys"), q};
availableKeysGroupBox->setFlat(true);
auto availableKeysLayout = new QVBoxLayout{availableKeysGroupBox};
{
auto hbox = new QHBoxLayout;
auto label = new QLabel{i18nc("@label", "Search:")};
label->setAccessibleName(i18nc("@label", "Search available keys"));
label->setToolTip(i18nc("@info:tooltip", "Search the list of available keys for keys matching the search term."));
hbox->addWidget(label);
ui.availableKeysFilter = new QLineEdit(q);
ui.availableKeysFilter->setClearButtonEnabled(true);
ui.availableKeysFilter->setAccessibleName(i18nc("@label", "Search available keys"));
ui.availableKeysFilter->setToolTip(i18nc("@info:tooltip", "Search the list of available keys for keys matching the search term."));
ui.availableKeysFilter->setPlaceholderText(i18nc("@info::placeholder", "Enter search term"));
ui.availableKeysFilter->setCursorPosition(0); // prevent emission of accessible text cursor event before accessible focus event
label->setBuddy(ui.availableKeysFilter);
hbox->addWidget(ui.availableKeysFilter, 1);
availableKeysLayout->addLayout(hbox);
}
availableKeysModel = AbstractKeyListModel::createFlatKeyListModel(q);
availableKeysModel->setKeys(KeyCache::instance()->keys());
auto proxyModel = new DisableNonEncryptionKeysProxyModel(q);
proxyModel->setSourceModel(availableKeysModel);
ui.availableKeysList = new KeyTreeView({}, nullptr, proxyModel, q, {});
ui.availableKeysList->view()->setAccessibleName(i18n("available keys"));
ui.availableKeysList->view()->setRootIsDecorated(false);
ui.availableKeysList->setFlatModel(availableKeysModel);
ui.availableKeysList->setHierarchicalView(false);
if (!Settings{}.cmsEnabled()) {
ui.availableKeysList->setKeyFilter(createOpenPGPOnlyKeyFilter());
}
availableKeysLayout->addWidget(ui.availableKeysList, /*stretch=*/1);
centerLayout->addWidget(availableKeysGroupBox, /*stretch=*/1);
auto buttonsLayout = new QHBoxLayout;
buttonsLayout->addStretch(1);
auto addButton = new QPushButton(q);
addButton->setIcon(QIcon::fromTheme(QStringLiteral("arrow-down")));
addButton->setAccessibleName(i18nc("@action:button", "Add Selected Keys"));
addButton->setToolTip(i18n("Add the selected keys to the group"));
addButton->setEnabled(false);
buttonsLayout->addWidget(addButton);
auto removeButton = new QPushButton(q);
removeButton->setIcon(QIcon::fromTheme(QStringLiteral("arrow-up")));
removeButton->setAccessibleName(i18nc("@action:button", "Remove Selected Keys"));
removeButton->setToolTip(i18n("Remove the selected keys from the group"));
removeButton->setEnabled(false);
buttonsLayout->addWidget(removeButton);
buttonsLayout->addStretch(1);
centerLayout->addLayout(buttonsLayout);
auto groupKeysGroupBox = new QGroupBox{i18nc("@title", "Group Keys"), q};
groupKeysGroupBox->setFlat(true);
auto groupKeysLayout = new QVBoxLayout{groupKeysGroupBox};
{
auto hbox = new QHBoxLayout;
auto label = new QLabel{i18nc("@label", "Search:")};
label->setAccessibleName(i18nc("@label", "Search group keys"));
label->setToolTip(i18nc("@info:tooltip", "Search the list of group keys for keys matching the search term."));
hbox->addWidget(label);
ui.groupKeysFilter = new QLineEdit(q);
ui.groupKeysFilter->setClearButtonEnabled(true);
ui.groupKeysFilter->setAccessibleName(i18nc("@label", "Search group keys"));
ui.groupKeysFilter->setToolTip(i18nc("@info:tooltip", "Search the list of group keys for keys matching the search term."));
ui.groupKeysFilter->setPlaceholderText(i18nc("@info::placeholder", "Enter search term"));
ui.groupKeysFilter->setCursorPosition(0); // prevent emission of accessible text cursor event before accessible focus event
label->setBuddy(ui.groupKeysFilter);
hbox->addWidget(ui.groupKeysFilter, 1);
groupKeysLayout->addLayout(hbox);
}
groupKeysModel = AbstractKeyListModel::createFlatKeyListModel(q);
auto warnNonEncryptionProxyModel = new WarnNonEncryptionKeysProxyModel(q);
ui.groupKeysList = new KeyTreeView({}, nullptr, warnNonEncryptionProxyModel, q, {});
ui.groupKeysList->view()->setAccessibleName(i18n("group keys"));
ui.groupKeysList->view()->setRootIsDecorated(false);
ui.groupKeysList->setFlatModel(groupKeysModel);
ui.groupKeysList->setHierarchicalView(false);
groupKeysLayout->addWidget(ui.groupKeysList, /*stretch=*/1);
centerLayout->addWidget(groupKeysGroupBox, /*stretch=*/1);
mainLayout->addLayout(centerLayout);
mainLayout->addWidget(new KSeparator(Qt::Horizontal, q));
ui.buttonBox = new QDialogButtonBox(QDialogButtonBox::Save | QDialogButtonBox::Cancel, q);
QPushButton *saveButton = ui.buttonBox->button(QDialogButtonBox::Save);
KGuiItem::assign(saveButton, KStandardGuiItem::save());
KGuiItem::assign(ui.buttonBox->button(QDialogButtonBox::Cancel), KStandardGuiItem::cancel());
saveButton->setEnabled(false);
mainLayout->addWidget(ui.buttonBox);
// prevent accidental closing of dialog when pressing Enter while a search field has focus
Kleo::unsetAutoDefaultButtons(q);
connect(ui.groupNameEdit, &QLineEdit::textChanged, q, [saveButton](const QString &text) {
saveButton->setEnabled(!text.trimmed().isEmpty());
});
connect(ui.availableKeysFilter, &QLineEdit::textChanged, ui.availableKeysList, &KeyTreeView::setStringFilter);
connect(ui.availableKeysList->view()->selectionModel(),
&QItemSelectionModel::selectionChanged,
q,
[addButton](const QItemSelection &selected, const QItemSelection &) {
addButton->setEnabled(!selected.isEmpty());
});
connect(ui.availableKeysList->view(), &QAbstractItemView::doubleClicked, q, [this](const QModelIndex &index) {
showKeyDetails(index);
});
connect(ui.groupKeysFilter, &QLineEdit::textChanged, ui.groupKeysList, &KeyTreeView::setStringFilter);
connect(ui.groupKeysList->view()->selectionModel(),
&QItemSelectionModel::selectionChanged,
q,
[removeButton](const QItemSelection &selected, const QItemSelection &) {
removeButton->setEnabled(!selected.isEmpty());
});
connect(ui.groupKeysList->view(), &QAbstractItemView::doubleClicked, q, [this](const QModelIndex &index) {
showKeyDetails(index);
});
connect(addButton, &QPushButton::clicked, q, [this]() {
addKeysToGroup();
});
connect(removeButton, &QPushButton::clicked, q, [this]() {
removeKeysFromGroup();
});
connect(ui.buttonBox, &QDialogButtonBox::accepted, q, &EditGroupDialog::accept);
connect(ui.buttonBox, &QDialogButtonBox::rejected, q, &EditGroupDialog::reject);
connect(KeyCache::instance().get(), &KeyCache::keysMayHaveChanged, q, [this] {
updateFromKeyCache();
});
// calculate default size with enough space for the key list
const auto fm = q->fontMetrics();
const QSize sizeHint = q->sizeHint();
const QSize defaultSize = QSize(qMax(sizeHint.width(), 150 * fm.horizontalAdvance(QLatin1Char('x'))), sizeHint.height());
restoreLayout(defaultSize);
}
~Private()
{
saveLayout();
}
private:
void saveLayout()
{
KConfigGroup configGroup(KSharedConfig::openConfig(), "EditGroupDialog");
configGroup.writeEntry("Size", q->size());
configGroup.sync();
}
void restoreLayout(const QSize &defaultSize)
{
const KConfigGroup configGroup(KSharedConfig::openConfig(), "EditGroupDialog");
const KConfigGroup availableKeysConfig = configGroup.group("AvailableKeysView");
ui.availableKeysList->restoreLayout(availableKeysConfig);
const KConfigGroup groupKeysConfig = configGroup.group("GroupKeysView");
ui.groupKeysList->restoreLayout(groupKeysConfig);
const QSize size = configGroup.readEntry("Size", defaultSize);
if (size.isValid()) {
q->resize(size);
}
}
void showKeyDetails(const QModelIndex &index)
{
if (!index.isValid()) {
return;
}
const auto key = index.model()->data(index, KeyList::KeyRole).value<GpgME::Key>();
if (!key.isNull()) {
auto cmd = new DetailsCommand(key);
cmd->setParentWidget(q);
cmd->start();
}
}
void addKeysToGroup();
void removeKeysFromGroup();
void updateFromKeyCache();
};
void EditGroupDialog::Private::addKeysToGroup()
{
const std::vector<Key> selectedGroupKeys = ui.groupKeysList->selectedKeys();
const std::vector<Key> selectedKeys = ui.availableKeysList->selectedKeys();
groupKeysModel->addKeys(selectedKeys);
for (const Key &key : selectedKeys) {
availableKeysModel->removeKey(key);
}
ui.groupKeysList->selectKeys(selectedGroupKeys);
}
void EditGroupDialog::Private::removeKeysFromGroup()
{
const auto selectedOtherKeys = ui.availableKeysList->selectedKeys();
const std::vector<Key> selectedKeys = ui.groupKeysList->selectedKeys();
for (const Key &key : selectedKeys) {
groupKeysModel->removeKey(key);
}
availableKeysModel->addKeys(selectedKeys);
ui.availableKeysList->selectKeys(selectedOtherKeys);
}
void EditGroupDialog::Private::updateFromKeyCache()
{
const auto selectedGroupKeys = ui.groupKeysList->selectedKeys();
const auto selectedOtherKeys = ui.availableKeysList->selectedKeys();
const auto oldGroupKeys = q->groupKeys();
const auto wasGroupKey = [oldGroupKeys](const Key &key) {
return Kleo::any_of(oldGroupKeys, [key](const auto &k) {
return _detail::ByFingerprint<std::equal_to>()(k, key);
});
};
const auto allKeys = KeyCache::instance()->keys();
std::vector<Key> groupKeys;
groupKeys.reserve(allKeys.size());
std::vector<Key> otherKeys;
otherKeys.reserve(otherKeys.size());
std::partition_copy(allKeys.begin(), allKeys.end(), std::back_inserter(groupKeys), std::back_inserter(otherKeys), wasGroupKey);
groupKeysModel->setKeys(groupKeys);
availableKeysModel->setKeys(otherKeys);
ui.groupKeysList->selectKeys(selectedGroupKeys);
ui.availableKeysList->selectKeys(selectedOtherKeys);
}
EditGroupDialog::EditGroupDialog(QWidget *parent)
: QDialog(parent)
, d(new Private(this))
{
setWindowTitle(i18nc("@title:window", "Edit Group"));
}
EditGroupDialog::~EditGroupDialog() = default;
void EditGroupDialog::setInitialFocus(FocusWidget widget)
{
switch (widget) {
case GroupName:
d->ui.groupNameEdit->setFocus();
break;
case KeysFilter:
d->ui.availableKeysFilter->setFocus();
break;
default:
qCDebug(KLEOPATRA_LOG) << "EditGroupDialog::setInitialFocus - invalid focus widget:" << widget;
}
}
void EditGroupDialog::setGroupName(const QString &name)
{
d->ui.groupNameEdit->setText(name);
}
QString EditGroupDialog::groupName() const
{
return d->ui.groupNameEdit->text().trimmed();
}
void EditGroupDialog::setGroupKeys(const std::vector<Key> &groupKeys)
{
d->groupKeysModel->setKeys(groupKeys);
// update the keys in the "available keys" list
const auto isGroupKey = [groupKeys](const Key &key) {
return Kleo::any_of(groupKeys, [key](const auto &k) {
return _detail::ByFingerprint<std::equal_to>()(k, key);
});
};
auto otherKeys = KeyCache::instance()->keys();
Kleo::erase_if(otherKeys, isGroupKey);
d->availableKeysModel->setKeys(otherKeys);
}
std::vector<Key> EditGroupDialog::groupKeys() const
{
std::vector<Key> keys;
keys.reserve(d->groupKeysModel->rowCount());
for (int row = 0; row < d->groupKeysModel->rowCount(); ++row) {
const QModelIndex index = d->groupKeysModel->index(row, 0);
keys.push_back(d->groupKeysModel->key(index));
}
return keys;
}
void EditGroupDialog::showEvent(QShowEvent *event)
{
QDialog::showEvent(event);
// prevent accidental closing of dialog when pressing Enter while a search field has focus
Kleo::unsetDefaultButtons(d->ui.buttonBox);
}
#include "editgroupdialog.moc"
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Dec 29, 7:20 AM (4 h, 5 m)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
92/34/8ef85b6fa6c49016c5a006980f69
Attached To
rKLEOPATRA Kleopatra
Event Timeline
Log In to Comment