diff --git a/src/commands/command.cpp b/src/commands/command.cpp index a03c97ea0..a9512ca9e 100644 --- a/src/commands/command.cpp +++ b/src/commands/command.cpp @@ -1,327 +1,327 @@ /* -*- mode: c++; c-basic-offset:4 -*- commands/command.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "command.h" #include "command_p.h" #include "checksumverifyfilescommand.h" #include "decryptverifyfilescommand.h" #include "detailscommand.h" #include "importcertificatefromfilecommand.h" #include "lookupcertificatescommand.h" #include "signencryptfilescommand.h" #include "viewemailfilescommand.h" #include #include #include #include "kleopatra_debug.h" #include #include #include using namespace Kleo; using namespace Kleo::Commands; using namespace GpgME; Command::Private::Private(Command *qq) : q(qq) , autoDelete(true) , warnWhenRunningAtShutdown(true) { } Command::Private::Private(Command *qq, KeyListController *controller) : q(qq) , autoDelete(true) , warnWhenRunningAtShutdown(true) , controller_(controller) { } Command::Private::Private(Command *qq, QWidget *parent) : q(qq) , autoDelete(true) , warnWhenRunningAtShutdown(true) , parentWidget_(parent) { } Command::Private::~Private() { qCDebug(KLEOPATRA_LOG) << q << __func__; } Command::Command(KeyListController *p) : QObject(p) , d(new Private(this, p)) { if (p) { p->registerCommand(this); } } Command::Command(QAbstractItemView *v, KeyListController *p) : QObject(p) , d(new Private(this, p)) { if (p) { p->registerCommand(this); } if (v) { setView(v); } } Command::Command(Private *pp) : QObject(pp->controller_) , d(pp) { if (pp->controller_) { pp->controller_->registerCommand(this); } } Command::Command(QAbstractItemView *v, Private *pp) : QObject(pp->controller_) , d(pp) { if (pp->controller_) { pp->controller_->registerCommand(this); } if (v) { setView(v); } } Command::Command(const GpgME::Key &key) : QObject(nullptr) , d(new Private(this)) { d->keys_ = std::vector(1, key); } Command::Command(const std::vector &keys) : QObject(nullptr) , d(new Private(this)) { d->keys_ = keys; } Command::Command(const Key &key, Private *pp) : QObject(nullptr) , d(pp) { d->keys_ = std::vector(1, key); } Command::Command(const std::vector &keys, Private *pp) : QObject(nullptr) , d(pp) { d->keys_ = keys; } Command::~Command() { qCDebug(KLEOPATRA_LOG) << this << __func__; } void Command::setAutoDelete(bool on) { d->autoDelete = on; } bool Command::autoDelete() const { return d->autoDelete; } void Command::setWarnWhenRunningAtShutdown(bool on) { d->warnWhenRunningAtShutdown = on; } bool Command::warnWhenRunningAtShutdown() const { return d->warnWhenRunningAtShutdown; } void Command::setParentWidget(QWidget *widget) { d->parentWidget_ = widget; } void Command::setParentWId(WId wid) { d->parentWId_ = wid; } void Command::setView(QAbstractItemView *view) { if (view == d->view_) { return; } d->view_ = view; if (!view || !d->keys_.empty()) { return; } const auto *const keyListModel = dynamic_cast(view->model()); if (!keyListModel) { qCWarning(KLEOPATRA_LOG) << "view" << view << "has not key list model"; return; } const QItemSelectionModel *const sm = view->selectionModel(); if (!sm) { qCWarning(KLEOPATRA_LOG) << "view" << view << "has no selection model"; return; } const QList selected = sm->selectedRows(); std::transform(selected.begin(), selected.end(), std::back_inserter(d->keys_), [keyListModel](const auto &idx) { return keyListModel->key(idx); }); } void Command::setKey(const Key &key) { d->keys_.clear(); if (!key.isNull()) { d->keys_.push_back(key); } } void Command::setKeys(const std::vector &keys) { d->keys_ = keys; } void Command::start() { // defer the actual start and return immediately to avoid problems if the // caller is deleted before start returns (e.g. an action of a context menu) QMetaObject::invokeMethod( this, [this]() { doStart(); }, Qt::QueuedConnection); } void Command::cancel() { qCDebug(KLEOPATRA_LOG) << metaObject()->className(); doCancel(); Q_EMIT canceled(); } void Command::addTemporaryView(const QString &title, AbstractKeyListSortFilterProxyModel *proxy, const QString &tabToolTip) { if (TabWidget *const tw = d->controller_ ? d->controller_->tabWidget() : nullptr) if (QAbstractItemView *const v = tw->addTemporaryView(title, proxy, tabToolTip)) { setView(v); } } void Command::applyWindowID(QWidget *w) const { if (w) { if (d->parentWId()) { if (QWidget *pw = QWidget::find(d->parentWId())) { // remember the current focus widget; re-parenting resets it QWidget *focusWidget = w->focusWidget(); w->setParent(pw, w->windowFlags()); if (focusWidget) { focusWidget->setFocus(); } } else { w->setAttribute(Qt::WA_NativeWindow, true); KWindowSystem::setMainWindow(w->windowHandle(), d->parentWId()); } } else { // remember the current focus widget; re-parenting resets it QWidget *focusWidget = w->focusWidget(); w->setParent(d->parentWidgetOrView(), w->windowFlags()); if (focusWidget) { focusWidget->setFocus(); } } } } // static -QList Command::commandsForFiles(const QStringList &files) +QList Command::commandsForFiles(const QStringList &files, KeyListController *controller) { QStringList importFiles, decryptFiles, encryptFiles, checksumFiles, emailFiles; QList cmds; for (const QString &fileName : files) { const unsigned int classification = classify(fileName); if (classification & Class::MimeFile) { emailFiles << fileName; } else if (classification & Class::AnyCertStoreType) { importFiles << fileName; } else if (classification & Class::AnyMessageType) { // For any message we decrypt / verify. This includes // the class CipherText decryptFiles << fileName; } else if (isChecksumFile(fileName)) { checksumFiles << fileName; } else { QFileInfo fi(fileName); if (fi.isReadable()) { encryptFiles << fileName; } } } if (!importFiles.isEmpty()) { - cmds << new ImportCertificateFromFileCommand(importFiles, nullptr); + cmds << new ImportCertificateFromFileCommand(importFiles, controller); } if (!decryptFiles.isEmpty()) { - cmds << new DecryptVerifyFilesCommand(decryptFiles, nullptr); + cmds << new DecryptVerifyFilesCommand(decryptFiles, controller); } if (!encryptFiles.isEmpty()) { - cmds << new SignEncryptFilesCommand(encryptFiles, nullptr); + cmds << new SignEncryptFilesCommand(encryptFiles, controller); } if (!checksumFiles.isEmpty()) { - cmds << new ChecksumVerifyFilesCommand(checksumFiles, nullptr); + cmds << new ChecksumVerifyFilesCommand(checksumFiles, controller); } if (!emailFiles.isEmpty()) { - cmds << new ViewEmailFilesCommand(emailFiles, nullptr); + cmds << new ViewEmailFilesCommand(emailFiles, controller); } return cmds; } // static Command *Command::commandForQuery(const QString &query) { const auto cache = Kleo::KeyCache::instance(); GpgME::Key key = cache->findByKeyIDOrFingerprint(query.toLocal8Bit().data()); if (key.isNull() && query.size() > 16) { // Try to find by subkeyid std::vector id; id.push_back(query.right(16).toStdString()); auto keys = cache->findSubkeysByKeyID(id); if (keys.size()) { key = keys[0].parent(); } } if (key.isNull()) { return new LookupCertificatesCommand(query, nullptr); } else { return new DetailsCommand(key); } } #include "moc_command.cpp" diff --git a/src/commands/command.h b/src/commands/command.h index b3bee32cc..b3da77b66 100644 --- a/src/commands/command.h +++ b/src/commands/command.h @@ -1,139 +1,139 @@ /* -*- mode: c++; c-basic-offset:4 -*- commands/command.h This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include #include // for WId #include #include // for ExecutionContext #include class QModelIndex; template class QList; class QAbstractItemView; namespace GpgME { class Key; } namespace Kleo { class KeyListController; class AbstractKeyListSortFilterProxyModel; class Command : public QObject, public ExecutionContext { Q_OBJECT public: explicit Command(KeyListController *parent); explicit Command(QAbstractItemView *view, KeyListController *parent); explicit Command(const GpgME::Key &key); explicit Command(const std::vector &keys); ~Command() override; enum Restriction { // clang-format off NoRestriction = 0x0000, NeedSelection = 0x0001, OnlyOneKey = 0x0002, NeedSecretKey = 0x0004, //< command performs secret key operations NeedSecretKeyData = 0x0008, //< command needs access to the secret key data MustBeOpenPGP = 0x0010, MustBeCMS = 0x0020, // esoteric: MayOnlyBeSecretKeyIfOwnerTrustIsNotYetUltimate = 0x0040, // for set-owner-trust AnyCardHasNullPin = 0x0080, MustBeRoot = 0x0200, MustBeTrustedRoot = 0x0400 | MustBeRoot, MustBeUntrustedRoot = 0x0800 | MustBeRoot, MustBeValid = 0x1000, //< key is neither revoked nor expired nor otherwise "bad" _AllRestrictions_Helper, AllRestrictions = 2 * (_AllRestrictions_Helper - 1) - 1, // clang-format on }; Q_DECLARE_FLAGS(Restrictions, Restriction) static Restrictions restrictions() { return NoRestriction; } /** Classify the files and return the most appropriate commands. * * @param files: A list of files. * * @returns null QString on success. Error message otherwise. */ - static QList commandsForFiles(const QStringList &files); + static QList commandsForFiles(const QStringList &files, KeyListController *controller); /** Get a command for a query. * * @param query: A keyid / fingerprint or any string to use in the search. */ static Command *commandForQuery(const QString &query); void setParentWidget(QWidget *widget); void setParentWId(WId wid); void setView(QAbstractItemView *view); void setKey(const GpgME::Key &key); void setKeys(const std::vector &keys); void setAutoDelete(bool on); bool autoDelete() const; void setWarnWhenRunningAtShutdown(bool warn); bool warnWhenRunningAtShutdown() const; public Q_SLOTS: void start(); void cancel(); Q_SIGNALS: void info(const QString &message, int timeout = 0); void progress(int current, int total); void finished(); void canceled(); private: virtual void doStart() = 0; virtual void doCancel() = 0; private: void applyWindowID(QWidget *wid) const override; protected: void addTemporaryView(const QString &title, AbstractKeyListSortFilterProxyModel *proxy = nullptr, const QString &tabToolTip = QString()); protected: class Private; kdtools::pimpl_ptr d; protected: explicit Command(Private *pp); explicit Command(QAbstractItemView *view, Private *pp); explicit Command(const std::vector &keys, Private *pp); explicit Command(const GpgME::Key &key, Private *pp); }; } Q_DECLARE_OPERATORS_FOR_FLAGS(Kleo::Command::Restrictions) diff --git a/src/commands/importcertificatescommand.cpp b/src/commands/importcertificatescommand.cpp index 816147cd7..c5fde091e 100644 --- a/src/commands/importcertificatescommand.cpp +++ b/src/commands/importcertificatescommand.cpp @@ -1,1090 +1,1097 @@ /* -*- 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 SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "importcertificatescommand.h" #include "importcertificatescommand_p.h" #include "certifycertificatecommand.h" #include "kleopatra_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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 &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 &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::const_iterator it = Kleo::binary_find(m_importsByFingerprint.begin(), m_importsByFingerprint.end(), fpr.toLatin1().constData(), ByImportFingerprint()); 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()); } private: void updateFindCache(const std::vector &results) { m_importsByFingerprint.clear(); m_idsByFingerprint.clear(); m_results = results; for (const auto &r : results) { const std::vector imports = r.result.imports(); m_importsByFingerprint.insert(m_importsByFingerprint.end(), imports.begin(), imports.end()); for (std::vector::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()); } private: mutable std::vector m_importsByFingerprint; mutable std::map, ByImportFingerprint> m_idsByFingerprint; std::vector 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 &ids) { QStringList escapedIds; for (const QString &id : ids) { if (!id.isEmpty()) { escapedIds << id.toHtmlEscaped(); } } return escapedIds.join(QLatin1StringView("
")); } static QString make_tooltip(const std::vector &results) { if (results.empty()) { return {}; } std::vector 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:
%1", format_ids(ids)); } void ImportCertificatesCommand::Private::setImportResultProxyModel(const std::vector &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(parentWidgetOrView())) { tv->expandAll(); } } int sum(const std::vector &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 &results, const std::vector &groups) { const KLocalizedString normalLine = ki18n("%1%2"); const KLocalizedString boldLine = ki18n("%1%2"); const KLocalizedString headerLine = ki18n("%1"); std::vector 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 not 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(QLatin1StringView{" "}).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(QLatin1StringView{}); } static bool isImportFromSingleSource(const std::vector &res) { return (res.size() == 1) || (res.size() == 2 && res[0].id == res[1].id); } static QString make_message_report(const std::vector &res, const std::vector &groups) { QString report{QLatin1StringView{""}}; 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 += QLatin1StringView{"

"} + title + QLatin1String{"

"}; report += QLatin1StringView{"

"}; report += make_report(res, groups); report += QLatin1StringView{"

"}; } report += QLatin1StringView{""}; 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("
") + i18n("Certifying means that you check the Fingerprint.") + QStringLiteral("
") + i18n("Some suggestions to do this are:") + QStringLiteral("
    • %1").arg(suggestions.join(QStringLiteral("
      "))) + QStringLiteral("
  • ") + 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 &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 &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("
    %1
    ").arg(headerLine); } if (r.auditLog.error().code() == GPG_ERR_NO_DATA) { s += QStringLiteral("") + i18nc("@info", "Audit log is empty.") + QStringLiteral(""); } else if (r.result.error().isCanceled()) { s += QStringLiteral("") + i18nc("@info", "Import was canceled.") + QStringLiteral(""); } else { s += r.auditLog.text(); } return s; }; std::transform(res.cbegin(), res.cend(), std::back_inserter(auditLogs), extractAndAnnotateAuditLog); return AuditLogEntry{auditLogs.join(QLatin1StringView{"
    "}), Error{}}; } } void ImportCertificatesCommand::Private::showDetails(const std::vector &res, const std::vector &groups) { const auto singleOpenPGPImport = getSingleOpenPGPImport(res); + + for (const auto &result : res) { + if (result.result.numImported() > 0) { + setImportResultProxyModel(res); + break; + } + } + 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( "

    An error occurred while trying to import the certificate:

    " "

    %1

    ", Formatting::errorAsString(err)); } else { return i18n( "

    An error occurred while trying to import the certificate %1:

    " "

    %2

    ", 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(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 &results, QWidget *dialog) { std::unordered_set 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%2").subs(temp).subs(Formatting::prettyNameAndEMail(uid)); }); const QString str = xi18nc("@info", "You have imported a certificate with fingerprint" "%1" "" "and user IDs" "%2" "" "Is this your own certificate?", 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 &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(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{}, [](auto allIds, const auto &r) { allIds.insert(r.id); return allIds; }); const auto canceledIds = std::accumulate(std::cbegin(results), std::cend(results), std::set{}, [](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 !Kleo::keyHasEncrypt(key); })) { KMessageBox::information(parent, xi18nc("@info", "The imported group%1contains certificates that cannot be used for encryption. " "This may lead to unexpected results.", 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 &fingerprints, const std::vector &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 &results) { return std::accumulate(std::begin(results), std::end(results), std::vector{}, [](auto fingerprints, const auto &r) { if (r.protocol == GpgME::OpenPGP) { fingerprints = accumulateNewKeys(fingerprints, r.result.imports()); } return fingerprints; }); } std::set ImportCertificatesCommand::Private::getMissingSignerKeyIds(const std::vector &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 &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 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(backend->importJob()); } else { return std::unique_ptr(); } } 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 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 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 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(backend->importFromKeyserverJob()); } else { return std::unique_ptr(); } } void ImportCertificatesCommand::Private::startImport(GpgME::Protocol protocol, const std::vector &keys, const QString &id) { Q_ASSERT(protocol != UnknownProtocol); if (std::find(nonWorkingProtocols.cbegin(), nonWorkingProtocols.cend(), protocol) != nonWorkingProtocols.cend()) { return; } std::unique_ptr 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 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 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 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/kleopatraapplication.cpp b/src/kleopatraapplication.cpp index cffa701d7..ed41f07fb 100644 --- a/src/kleopatraapplication.cpp +++ b/src/kleopatraapplication.cpp @@ -1,870 +1,870 @@ /* kleopatraapplication.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2016 Bundesamt für Sicherheit in der Informationstechnik SPDX-FileContributor: Intevation GmbH SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "kleopatraapplication.h" #include "kleopatra_options.h" #include "mainwindow.h" #include "settings.h" #include "smimevalidationpreferences.h" #include "systrayicon.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "commands/checksumcreatefilescommand.h" #include "commands/checksumverifyfilescommand.h" #include "commands/decryptverifyfilescommand.h" #include "commands/detailscommand.h" #include "commands/importcertificatefromfilecommand.h" #include "commands/lookupcertificatescommand.h" #include "commands/newcertificatesigningrequestcommand.h" #include "commands/newopenpgpcertificatecommand.h" #include "commands/signencryptfilescommand.h" #include "dialogs/updatenotification.h" #include "kleopatra_debug.h" #include #include #include #include #include #include #include #include #include #if QT_CONFIG(graphicseffect) #include #endif #include #include #include #include #include #include #ifdef Q_OS_WIN #include #endif using namespace Kleo; using namespace Kleo::Commands; static void add_resources() { KIconLoader::global()->addAppDir(QStringLiteral("libkleopatra")); KIconLoader::global()->addAppDir(QStringLiteral("kwatchgnupg")); } static QList default_logging_options() { QList result; result.push_back("io"); return result; } namespace { class FocusFrame : public QFocusFrame { Q_OBJECT public: using QFocusFrame::QFocusFrame; protected: void paintEvent(QPaintEvent *event) override; }; static QRect effectiveWidgetRect(const QWidget *w) { // based on QWidgetPrivate::effectiveRectFor #if QT_CONFIG(graphicseffect) if (auto graphicsEffect = w->graphicsEffect(); graphicsEffect && graphicsEffect->isEnabled()) return graphicsEffect->boundingRectFor(w->rect()).toAlignedRect(); #endif // QT_CONFIG(graphicseffect) return w->rect(); } static QRect clipRect(const QWidget *w) { // based on QWidgetPrivate::clipRect if (!w->isVisible()) { return QRect(); } QRect r = effectiveWidgetRect(w); int ox = 0; int oy = 0; while (w && w->isVisible() && !w->isWindow() && w->parentWidget()) { ox -= w->x(); oy -= w->y(); w = w->parentWidget(); r &= QRect(ox, oy, w->width(), w->height()); } return r; } void FocusFrame::paintEvent(QPaintEvent *) { if (!widget()) { return; } QStylePainter p(this); QStyleOptionFocusRect option; initStyleOption(&option); const int vmargin = style()->pixelMetric(QStyle::PM_FocusFrameVMargin, &option); const int hmargin = style()->pixelMetric(QStyle::PM_FocusFrameHMargin, &option); const QRect rect = clipRect(widget()).adjusted(0, 0, hmargin * 2, vmargin * 2); p.setClipRect(rect); p.drawPrimitive(QStyle::PE_FrameFocusRect, option); } } class KleopatraApplication::Private { friend class ::KleopatraApplication; KleopatraApplication *const q; public: explicit Private(KleopatraApplication *qq) : q(qq) , ignoreNewInstance(true) , firstNewInstance(true) , sysTray(nullptr) , groupConfig{std::make_shared(QStringLiteral("kleopatragroupsrc"))} { } ~Private() { #ifndef QT_NO_SYSTEMTRAYICON delete sysTray; #endif } void setUpSysTrayIcon() { #ifndef QT_NO_SYSTEMTRAYICON Q_ASSERT(readerStatus); sysTray = new SysTrayIcon(); sysTray->setFirstCardWithNullPin(readerStatus->firstCardWithNullPin()); connect(readerStatus.get(), &SmartCard::ReaderStatus::firstCardWithNullPinChanged, sysTray, &SysTrayIcon::setFirstCardWithNullPin); #endif } private: void connectConfigureDialog() { if (configureDialog) { if (q->mainWindow()) { connect(configureDialog, SIGNAL(configCommitted()), q->mainWindow(), SLOT(slotConfigCommitted())); } connect(configureDialog, &ConfigureDialog::configCommitted, q, &KleopatraApplication::configurationChanged); } } void disconnectConfigureDialog() { if (configureDialog) { if (q->mainWindow()) { disconnect(configureDialog, SIGNAL(configCommitted()), q->mainWindow(), SLOT(slotConfigCommitted())); } disconnect(configureDialog, &ConfigureDialog::configCommitted, q, &KleopatraApplication::configurationChanged); } } public: bool ignoreNewInstance; bool firstNewInstance; QPointer focusFrame; QPointer configureDialog; QPointer groupsConfigDialog; QPointer mainWindow; std::unique_ptr readerStatus; #ifndef QT_NO_SYSTEMTRAYICON SysTrayIcon *sysTray; #endif std::shared_ptr groupConfig; std::shared_ptr keyCache; std::shared_ptr log; std::shared_ptr watcher; std::shared_ptr distroSettings; public: void setupKeyCache() { keyCache = KeyCache::mutableInstance(); keyCache->setRefreshInterval(SMimeValidationPreferences{}.refreshInterval()); watcher.reset(new FileSystemWatcher); watcher->whitelistFiles(gnupgFileWhitelist()); watcher->addPaths(gnupgFolderWhitelist()); watcher->setDelay(1000); keyCache->addFileSystemWatcher(watcher); keyCache->setGroupConfig(groupConfig); keyCache->setGroupsEnabled(Settings().groupsEnabled()); // always enable remarks (aka tags); in particular, this triggers a // relisting of the keys with signatures and signature notations // after the initial (fast) key listing keyCache->enableRemarks(true); } void setUpFilterManager() { if (!Settings{}.cmsEnabled()) { KeyFilterManager::instance()->alwaysFilterByProtocol(GpgME::OpenPGP); } } void setupLogging() { log = Log::mutableInstance(); const QByteArray envOptions = qgetenv("KLEOPATRA_LOGOPTIONS"); const bool logAll = envOptions.trimmed() == "all"; const QList options = envOptions.isEmpty() ? default_logging_options() : envOptions.split(','); const QByteArray dirNative = qgetenv("KLEOPATRA_LOGDIR"); if (dirNative.isEmpty()) { return; } const QString dir = QFile::decodeName(dirNative); const QString logFileName = QDir(dir).absoluteFilePath(QStringLiteral("kleopatra.log.%1").arg(QCoreApplication::applicationPid())); std::unique_ptr logFile(new QFile(logFileName)); if (!logFile->open(QIODevice::WriteOnly | QIODevice::Append)) { qCDebug(KLEOPATRA_LOG) << "Could not open file for logging: " << logFileName << "\nLogging disabled"; return; } log->setOutputDirectory(dir); if (logAll || options.contains("io")) { log->setIOLoggingEnabled(true); } qInstallMessageHandler(Log::messageHandler); if (logAll || options.contains("pipeio")) { KDPipeIODevice::setDebugLevel(KDPipeIODevice::Debug); } UiServer::setLogStream(log->logFile()); } void updateFocusFrame(QWidget *focusWidget) { if (focusWidget && focusWidget->inherits("QLabel") && focusWidget->window()->testAttribute(Qt::WA_KeyboardFocusChange)) { if (!focusFrame) { focusFrame = new FocusFrame{focusWidget}; } focusFrame->setWidget(focusWidget); } else if (focusFrame) { focusFrame->setWidget(nullptr); } } }; KleopatraApplication::KleopatraApplication(int &argc, char *argv[]) : QApplication(argc, argv) , d(new Private(this)) { // disable parent<->child navigation in tree views with left/right arrow keys // because this interferes with column by column navigation that is required // for accessibility setStyleSheet(QStringLiteral("QTreeView { arrow-keys-navigate-into-children: 0; }")); connect(this, &QApplication::focusChanged, this, [this](QWidget *, QWidget *now) { d->updateFocusFrame(now); }); } void KleopatraApplication::init() { #ifdef Q_OS_WIN QWindowsWindowFunctions::setWindowActivationBehavior(QWindowsWindowFunctions::AlwaysActivateWindow); #endif const auto blockedUrlSchemes = Settings{}.blockedUrlSchemes(); for (const auto &scheme : blockedUrlSchemes) { QDesktopServices::setUrlHandler(scheme, this, "blockUrl"); } add_resources(); DN::setAttributeOrder(Settings{}.attributeOrder()); /* Start the gpg-agent early, this is done explicitly * because on an empty keyring our keylistings wont start * the agent. In that case any assuan-connect calls to * the agent will fail. The requested start via the * connection is additionally done in case the gpg-agent * is killed while Kleopatra is running. */ startGpgAgent(); d->readerStatus.reset(new SmartCard::ReaderStatus); connect(d->readerStatus.get(), &SmartCard::ReaderStatus::startOfGpgAgentRequested, this, &KleopatraApplication::startGpgAgent); d->setupKeyCache(); d->setUpSysTrayIcon(); d->setUpFilterManager(); d->setupLogging(); #ifdef Q_OS_WIN if (!SystemInfo::isHighContrastModeActive()) { /* In high contrast mode we do not want our own colors */ new KColorSchemeManager(this); } #else new KColorSchemeManager(this); #endif #ifndef QT_NO_SYSTEMTRAYICON d->sysTray->show(); #endif setQuitOnLastWindowClosed(false); // Sync config when we are about to quit connect(this, &QApplication::aboutToQuit, this, []() { KSharedConfig::openConfig()->sync(); }); } KleopatraApplication::~KleopatraApplication() { delete d->groupsConfigDialog; delete d->mainWindow; } namespace { using Func = void (KleopatraApplication::*)(const QStringList &, GpgME::Protocol); } void KleopatraApplication::slotActivateRequested(const QStringList &arguments, const QString &workingDirectory) { QCommandLineParser parser; kleopatra_options(&parser); QString err; if (!arguments.isEmpty() && !parser.parse(arguments)) { err = parser.errorText(); } else if (arguments.isEmpty()) { // KDBusServices omits the application name if no other // arguments are provided. In that case the parser prints // a warning. parser.parse(QStringList() << QCoreApplication::applicationFilePath()); } if (err.isEmpty()) { err = newInstance(parser, workingDirectory); } if (!err.isEmpty()) { KMessageBox::error(nullptr, err.toHtmlEscaped(), i18n("Failed to execute command")); Q_EMIT setExitValue(1); return; } Q_EMIT setExitValue(0); } QString KleopatraApplication::newInstance(const QCommandLineParser &parser, const QString &workingDirectory) { if (d->ignoreNewInstance) { qCDebug(KLEOPATRA_LOG) << "New instance ignored because of ignoreNewInstance"; return QString(); } QStringList files; const QDir cwd = QDir(workingDirectory); bool queryMode = parser.isSet(QStringLiteral("query")) || parser.isSet(QStringLiteral("search")); // Query and Search treat positional arguments differently, see below. if (!queryMode) { const auto positionalArguments = parser.positionalArguments(); for (const QString &file : positionalArguments) { // We do not check that file exists here. Better handle // these errors in the UI. if (QFileInfo(file).isAbsolute()) { files << file; } else { files << cwd.absoluteFilePath(file); } } } GpgME::Protocol protocol = GpgME::UnknownProtocol; if (parser.isSet(QStringLiteral("openpgp"))) { qCDebug(KLEOPATRA_LOG) << "found OpenPGP"; protocol = GpgME::OpenPGP; } if (parser.isSet(QStringLiteral("cms"))) { qCDebug(KLEOPATRA_LOG) << "found CMS"; if (protocol == GpgME::OpenPGP) { return i18n("Ambiguous protocol: --openpgp and --cms"); } protocol = GpgME::CMS; } // Check for Parent Window id WId parentId = 0; if (parser.isSet(QStringLiteral("parent-windowid"))) { #ifdef Q_OS_WIN // WId is not a portable type as it is a pointer type on Windows. // casting it from an integer is ok though as the values are guaranteed to // be compatible in the documentation. parentId = reinterpret_cast(parser.value(QStringLiteral("parent-windowid")).toUInt()); #else parentId = parser.value(QStringLiteral("parent-windowid")).toUInt(); #endif } // Handle openpgp4fpr URI scheme QString needle; if (queryMode) { needle = parser.positionalArguments().join(QLatin1Char(' ')); } if (needle.startsWith(QLatin1StringView("openpgp4fpr:"))) { needle.remove(0, 12); } // Check for --search command. if (parser.isSet(QStringLiteral("search"))) { // This is an extra command instead of a combination with the // similar query to avoid changing the older query commands behavior // and query's "show details if a certificate exist or search on a // keyserver" logic is hard to explain and use consistently. if (needle.isEmpty()) { return i18n("No search string specified for --search"); } auto const cmd = new LookupCertificatesCommand(needle, nullptr); cmd->setParentWId(parentId); cmd->start(); return QString(); } // Check for --query command if (parser.isSet(QStringLiteral("query"))) { if (needle.isEmpty()) { return i18n("No fingerprint argument specified for --query"); } auto cmd = Command::commandForQuery(needle); cmd->setParentWId(parentId); cmd->start(); return QString(); } // Check for --gen-key command if (parser.isSet(QStringLiteral("gen-key"))) { if (protocol == GpgME::CMS) { const Kleo::Settings settings{}; if (settings.cmsEnabled() && settings.cmsCertificateCreationAllowed()) { auto cmd = new NewCertificateSigningRequestCommand; cmd->setParentWId(parentId); cmd->start(); } else { return i18n("You are not allowed to create S/MIME certificate signing requests."); } } else { auto cmd = new NewOpenPGPCertificateCommand; cmd->setParentWId(parentId); cmd->start(); } return QString(); } // Check for --config command if (parser.isSet(QStringLiteral("config"))) { openConfigDialogWithForeignParent(parentId); return QString(); } struct FuncInfo { QString optionName; Func func; }; // While most of these options can be handled by the content autodetection // below it might be useful to override the autodetection if the input is in // doubt and you e.g. only want to import .asc files or fail and not decrypt them // if they are actually encrypted data. static const std::vector funcMap{ {QStringLiteral("import-certificate"), &KleopatraApplication::importCertificatesFromFile}, {QStringLiteral("encrypt"), &KleopatraApplication::encryptFiles}, {QStringLiteral("sign"), &KleopatraApplication::signFiles}, {QStringLiteral("encrypt-sign"), &KleopatraApplication::signEncryptFiles}, {QStringLiteral("sign-encrypt"), &KleopatraApplication::signEncryptFiles}, {QStringLiteral("decrypt"), &KleopatraApplication::decryptFiles}, {QStringLiteral("verify"), &KleopatraApplication::verifyFiles}, {QStringLiteral("decrypt-verify"), &KleopatraApplication::decryptVerifyFiles}, {QStringLiteral("checksum"), &KleopatraApplication::checksumFiles}, }; QString found; Func foundFunc = nullptr; for (const auto &[opt, fn] : funcMap) { if (parser.isSet(opt) && found.isEmpty()) { found = opt; foundFunc = fn; } else if (parser.isSet(opt)) { return i18n(R"(Ambiguous commands "%1" and "%2")", found, opt); } } QStringList errors; if (!found.isEmpty()) { if (files.empty()) { return i18n("No files specified for \"%1\" command", found); } qCDebug(KLEOPATRA_LOG) << "found" << found; (this->*foundFunc)(files, protocol); } else { if (files.empty()) { if (!(d->firstNewInstance && isSessionRestored())) { qCDebug(KLEOPATRA_LOG) << "openOrRaiseMainWindow"; openOrRaiseMainWindow(); } } else { for (const QString &fileName : std::as_const(files)) { QFileInfo fi(fileName); if (!fi.isReadable()) { errors << i18n("Cannot read \"%1\"", fileName); } } handleFiles(files, parentId); } } d->firstNewInstance = false; #ifdef Q_OS_WIN // On Windows we might be started from the // explorer in any working directory. E.g. // a double click on a file. To avoid preventing // the folder from deletion we set the // working directory to the users homedir. QDir::setCurrent(QDir::homePath()); #endif return errors.join(QLatin1Char('\n')); } void KleopatraApplication::handleFiles(const QStringList &files, WId parentId) { - const QList allCmds = Command::commandsForFiles(files); + const QList allCmds = Command::commandsForFiles(files, mainWindow()->keyListController()); for (Command *cmd : allCmds) { if (parentId) { cmd->setParentWId(parentId); } else { MainWindow *mw = mainWindow(); if (!mw) { mw = new MainWindow; mw->setAttribute(Qt::WA_DeleteOnClose); setMainWindow(mw); d->connectConfigureDialog(); } cmd->setParentWidget(mw); } if (dynamic_cast(cmd)) { openOrRaiseMainWindow(); } cmd->start(); } } #ifndef QT_NO_SYSTEMTRAYICON const SysTrayIcon *KleopatraApplication::sysTrayIcon() const { return d->sysTray; } SysTrayIcon *KleopatraApplication::sysTrayIcon() { return d->sysTray; } #endif const MainWindow *KleopatraApplication::mainWindow() const { return d->mainWindow; } MainWindow *KleopatraApplication::mainWindow() { return d->mainWindow; } void KleopatraApplication::setMainWindow(MainWindow *mainWindow) { if (mainWindow == d->mainWindow) { return; } d->disconnectConfigureDialog(); d->mainWindow = mainWindow; #ifndef QT_NO_SYSTEMTRAYICON d->sysTray->setMainWindow(mainWindow); #endif d->connectConfigureDialog(); } static void open_or_raise(QWidget *w) { #ifdef Q_OS_WIN if (w->isMinimized()) { qCDebug(KLEOPATRA_LOG) << __func__ << "unminimizing and raising window"; w->raise(); } else if (w->isVisible()) { qCDebug(KLEOPATRA_LOG) << __func__ << "raising window"; w->raise(); #else if (w->isVisible()) { qCDebug(KLEOPATRA_LOG) << __func__ << "activating window"; KWindowSystem::updateStartupId(w->windowHandle()); KWindowSystem::activateWindow(w->windowHandle()); #endif } else { qCDebug(KLEOPATRA_LOG) << __func__ << "showing window"; w->show(); } } void KleopatraApplication::toggleMainWindowVisibility() { if (mainWindow()) { mainWindow()->setVisible(!mainWindow()->isVisible()); } else { openOrRaiseMainWindow(); } if (mainWindow()->isVisible()) { mainWindow()->exportWindow(); } else { mainWindow()->unexportWindow(); } } void KleopatraApplication::restoreMainWindow() { qCDebug(KLEOPATRA_LOG) << "restoring main window"; // Sanity checks if (!isSessionRestored()) { qCDebug(KLEOPATRA_LOG) << "Not in session restore"; return; } if (mainWindow()) { qCDebug(KLEOPATRA_LOG) << "Already have main window"; return; } auto mw = new MainWindow; if (KMainWindow::canBeRestored(1)) { // restore to hidden state, Mainwindow::readProperties() will // restore saved visibility. mw->restore(1, false); } mw->setAttribute(Qt::WA_DeleteOnClose); setMainWindow(mw); d->connectConfigureDialog(); } void KleopatraApplication::openOrRaiseMainWindow() { MainWindow *mw = mainWindow(); if (!mw) { mw = new MainWindow; mw->setAttribute(Qt::WA_DeleteOnClose); setMainWindow(mw); d->connectConfigureDialog(); } open_or_raise(mw); UpdateNotification::checkUpdate(mw); } void KleopatraApplication::openConfigDialogWithForeignParent(WId parentWId) { if (!d->configureDialog) { d->configureDialog = new ConfigureDialog; d->configureDialog->setAttribute(Qt::WA_DeleteOnClose); d->connectConfigureDialog(); } // This is similar to what the commands do. if (parentWId) { if (QWidget *pw = QWidget::find(parentWId)) { d->configureDialog->setParent(pw, d->configureDialog->windowFlags()); } else { d->configureDialog->setAttribute(Qt::WA_NativeWindow, true); KWindowSystem::setMainWindow(d->configureDialog->windowHandle(), parentWId); } } open_or_raise(d->configureDialog); // If we have a parent we want to raise over it. if (parentWId) { d->configureDialog->raise(); } } void KleopatraApplication::openOrRaiseConfigDialog() { openConfigDialogWithForeignParent(0); } void KleopatraApplication::openOrRaiseGroupsConfigDialog(QWidget *parent) { if (!d->groupsConfigDialog) { d->groupsConfigDialog = new GroupsConfigDialog{parent}; d->groupsConfigDialog->setAttribute(Qt::WA_DeleteOnClose); } else { // reparent the dialog to ensure it's shown on top of the (modal) parent d->groupsConfigDialog->setParent(parent, Qt::Dialog); } open_or_raise(d->groupsConfigDialog); } #ifndef QT_NO_SYSTEMTRAYICON void KleopatraApplication::startMonitoringSmartCard() { Q_ASSERT(d->readerStatus); d->readerStatus->startMonitoring(); } #endif // QT_NO_SYSTEMTRAYICON void KleopatraApplication::importCertificatesFromFile(const QStringList &files, GpgME::Protocol /*proto*/) { openOrRaiseMainWindow(); if (!files.empty()) { mainWindow()->importCertificatesFromFile(files); } } void KleopatraApplication::encryptFiles(const QStringList &files, GpgME::Protocol proto) { auto const cmd = new SignEncryptFilesCommand(files, nullptr); cmd->setEncryptionPolicy(Force); cmd->setSigningPolicy(Allow); if (proto != GpgME::UnknownProtocol) { cmd->setProtocol(proto); } cmd->start(); } void KleopatraApplication::signFiles(const QStringList &files, GpgME::Protocol proto) { auto const cmd = new SignEncryptFilesCommand(files, nullptr); cmd->setSigningPolicy(Force); cmd->setEncryptionPolicy(Deny); if (proto != GpgME::UnknownProtocol) { cmd->setProtocol(proto); } cmd->start(); } void KleopatraApplication::signEncryptFiles(const QStringList &files, GpgME::Protocol proto) { auto const cmd = new SignEncryptFilesCommand(files, nullptr); if (proto != GpgME::UnknownProtocol) { cmd->setProtocol(proto); } cmd->start(); } void KleopatraApplication::decryptFiles(const QStringList &files, GpgME::Protocol /*proto*/) { auto const cmd = new DecryptVerifyFilesCommand(files, nullptr); cmd->setOperation(Decrypt); cmd->start(); } void KleopatraApplication::verifyFiles(const QStringList &files, GpgME::Protocol /*proto*/) { auto const cmd = new DecryptVerifyFilesCommand(files, nullptr); cmd->setOperation(Verify); cmd->start(); } void KleopatraApplication::decryptVerifyFiles(const QStringList &files, GpgME::Protocol /*proto*/) { auto const cmd = new DecryptVerifyFilesCommand(files, nullptr); cmd->start(); } void KleopatraApplication::checksumFiles(const QStringList &files, GpgME::Protocol /*proto*/) { QStringList verifyFiles, createFiles; for (const QString &file : files) { if (isChecksumFile(file)) { verifyFiles << file; } else { createFiles << file; } } if (!verifyFiles.isEmpty()) { auto const cmd = new ChecksumVerifyFilesCommand(verifyFiles, nullptr); cmd->start(); } if (!createFiles.isEmpty()) { auto const cmd = new ChecksumCreateFilesCommand(createFiles, nullptr); cmd->start(); } } void KleopatraApplication::setIgnoreNewInstance(bool ignore) { d->ignoreNewInstance = ignore; } bool KleopatraApplication::ignoreNewInstance() const { return d->ignoreNewInstance; } void KleopatraApplication::blockUrl(const QUrl &url) { qCDebug(KLEOPATRA_LOG) << "Blocking URL" << url; KMessageBox::error(mainWindow(), i18n("Opening an external link is administratively prohibited."), i18n("Prohibited")); } void KleopatraApplication::startGpgAgent() { Kleo::launchGpgAgent(); } void KleopatraApplication::setDistributionSettings(const std::shared_ptr &settings) { d->distroSettings = settings; } std::shared_ptr KleopatraApplication::distributionSettings() const { return d->distroSettings; } #include "kleopatraapplication.moc" #include "moc_kleopatraapplication.cpp" diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index f77a62be0..4bbeec8a0 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1,938 +1,943 @@ /* -*- mode: c++; c-basic-offset:4 -*- mainwindow.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "aboutdata.h" #include "kleopatraapplication.h" #include "mainwindow.h" #include "settings.h" #include #include "view/keycacheoverlay.h" #include "view/keylistcontroller.h" #include "view/padwidget.h" #include "view/searchbar.h" #include "view/smartcardwidget.h" #include "view/tabwidget.h" #include "view/welcomewidget.h" #include "commands/decryptverifyfilescommand.h" #include "commands/importcertificatefromfilecommand.h" #include "commands/importcrlcommand.h" #include "commands/selftestcommand.h" #include "commands/signencryptfilescommand.h" #include "utils/action_data.h" #include "utils/clipboardmenu.h" #include "utils/detail_p.h" #include "utils/filedialog.h" #include "utils/gui-helper.h" #include "utils/keyexportdraghandler.h" #include #include "dialogs/updatenotification.h" // needed for GPGME_VERSION_NUMBER #include #include "kleopatra_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_UNIX #include #endif #include #include using namespace std::chrono_literals; using namespace Kleo; using namespace Kleo::Commands; using namespace GpgME; static KGuiItem KStandardGuiItem_quit() { static const QString app = KAboutData::applicationData().displayName(); KGuiItem item = KStandardGuiItem::quit(); item.setText(xi18nc("@action:button", "&Quit %1", app)); return item; } static KGuiItem KStandardGuiItem_close() { KGuiItem item = KStandardGuiItem::close(); item.setText(i18nc("@action:button", "Only &Close Window")); return item; } static bool isQuitting = false; namespace { static const std::vector mainViewActionNames = { QStringLiteral("view_certificate_overview"), QStringLiteral("manage_smartcard"), QStringLiteral("pad_view"), }; class CertificateView : public QWidget, public FocusFirstChild { Q_OBJECT public: CertificateView(QWidget *parent = nullptr) : QWidget{parent} , ui{this} { } SearchBar *searchBar() const { return ui.searchBar; } TabWidget *tabWidget() const { return ui.tabWidget; } void focusFirstChild(Qt::FocusReason reason) override { ui.searchBar->lineEdit()->setFocus(reason); } private: struct UI { TabWidget *tabWidget = nullptr; SearchBar *searchBar = nullptr; explicit UI(CertificateView *q) { auto vbox = new QVBoxLayout{q}; vbox->setSpacing(0); searchBar = new SearchBar{q}; vbox->addWidget(searchBar); tabWidget = new TabWidget{q}; vbox->addWidget(tabWidget); tabWidget->connectSearchBar(searchBar); } } ui; }; } class MainWindow::Private { friend class ::MainWindow; MainWindow *const q; public: explicit Private(MainWindow *qq); ~Private(); template void createAndStart() { (new T(this->currentView(), &this->controller))->start(); } template void createAndStart(QAbstractItemView *view) { (new T(view, &this->controller))->start(); } template void createAndStart(const QStringList &a) { (new T(a, this->currentView(), &this->controller))->start(); } template void createAndStart(const QStringList &a, QAbstractItemView *view) { (new T(a, view, &this->controller))->start(); } void closeAndQuit() { const QString app = KAboutData::applicationData().displayName(); const int rc = KMessageBox::questionTwoActionsCancel(q, xi18n("%1 may be used by other applications as a service." "You may instead want to close this window without exiting %1.", app), i18nc("@title:window", "Really Quit?"), KStandardGuiItem_close(), KStandardGuiItem_quit(), KStandardGuiItem::cancel(), QLatin1StringView("really-quit-") + app.toLower()); if (rc == KMessageBox::Cancel) { return; } isQuitting = true; if (!q->close()) { return; } // WARNING: 'this' might be deleted at this point! if (rc == KMessageBox::ButtonCode::SecondaryAction) { qApp->quit(); } } void configureToolbars() { KEditToolBar dlg(q->factory()); dlg.exec(); } void editKeybindings() { KShortcutsDialog::showDialog(q->actionCollection(), KShortcutsEditor::LetterShortcutsAllowed, q); updateSearchBarClickMessage(); } void updateSearchBarClickMessage() { const QString shortcutStr = focusToClickSearchAction->shortcut().toString(); ui.searchTab->searchBar()->updateClickMessage(shortcutStr); } void updateStatusBar() { auto statusBar = std::make_unique(); auto settings = KleopatraApplication::instance()->distributionSettings(); bool showStatusbar = false; if (settings) { const QString statusline = settings->value(QStringLiteral("statusline"), {}).toString(); if (!statusline.isEmpty()) { auto customStatusLbl = new QLabel(statusline); statusBar->insertWidget(0, customStatusLbl); showStatusbar = true; } } if (DeVSCompliance::isActive()) { auto statusLbl = std::make_unique(DeVSCompliance::name()); if (!SystemInfo::isHighContrastModeActive()) { const auto color = KColorScheme(QPalette::Active, KColorScheme::View) .foreground(DeVSCompliance::isCompliant() ? KColorScheme::NormalText : KColorScheme::NegativeText) .color(); const auto background = KColorScheme(QPalette::Active, KColorScheme::View) .background(DeVSCompliance::isCompliant() ? KColorScheme::PositiveBackground : KColorScheme::NegativeBackground) .color(); statusLbl->setStyleSheet(QStringLiteral("QLabel { color: %1; background-color: %2; }").arg(color.name()).arg(background.name())); } statusBar->insertPermanentWidget(0, statusLbl.release()); showStatusbar = true; } if (showStatusbar) { q->setStatusBar(statusBar.release()); // QMainWindow takes ownership } else { q->setStatusBar(nullptr); } } void selfTest() { createAndStart(); } void configureGroups() { // open groups config dialog as independent top-level window KleopatraApplication::instance()->openOrRaiseGroupsConfigDialog(nullptr); } void showHandbook(); void gnupgLogViewer() { // Warning: Don't assume that the program needs to be in PATH. On Windows, it will also be found next to the calling process. if (!QProcess::startDetached(QStringLiteral("kwatchgnupg"), QStringList())) KMessageBox::error(q, i18n("Could not start the GnuPG Log Viewer (kwatchgnupg). " "Please check your installation."), i18n("Error Starting KWatchGnuPG")); } void forceUpdateCheck() { UpdateNotification::forceUpdateCheck(q); } void slotConfigCommitted(); void slotContextMenuRequested(QAbstractItemView *, const QPoint &p) { if (auto const menu = qobject_cast(q->factory()->container(QStringLiteral("listview_popup"), q))) { menu->exec(p); } else { qCDebug(KLEOPATRA_LOG) << "no \"listview_popup\" in kleopatra's ui.rc file"; } } void slotFocusQuickSearch() { ui.searchTab->searchBar()->lineEdit()->setFocus(); } void showView(const QString &actionName, QWidget *widget) { const auto coll = q->actionCollection(); if (coll) { for (const QString &name : mainViewActionNames) { if (auto action = coll->action(name)) { action->setChecked(name == actionName); } } } ui.stackWidget->setCurrentWidget(widget); if (auto ffci = dynamic_cast(widget)) { ffci->focusFirstChild(Qt::TabFocusReason); } } void showCertificateView() { if (KeyCache::instance()->keys().empty()) { showView(QStringLiteral("view_certificate_overview"), ui.welcomeWidget); } else { showView(QStringLiteral("view_certificate_overview"), ui.searchTab); } } void showSmartcardView() { showView(QStringLiteral("manage_smartcard"), ui.scWidget); } void showPadView() { if (!ui.padWidget) { ui.padWidget = new PadWidget; ui.stackWidget->addWidget(ui.padWidget); } showView(QStringLiteral("pad_view"), ui.padWidget); ui.stackWidget->resize(ui.padWidget->sizeHint()); } void restartDaemons() { Kleo::restartGpgAgent(); } private: void setupActions(); QAbstractItemView *currentView() const { return ui.searchTab->tabWidget()->currentView(); } void keyListingDone() { const auto curWidget = ui.stackWidget->currentWidget(); if (curWidget == ui.scWidget || curWidget == ui.padWidget) { return; } showCertificateView(); } private: Kleo::KeyListController controller; bool firstShow : 1; struct UI { CertificateView *searchTab = nullptr; PadWidget *padWidget = nullptr; SmartCardWidget *scWidget = nullptr; WelcomeWidget *welcomeWidget = nullptr; QStackedWidget *stackWidget = nullptr; explicit UI(MainWindow *q); } ui; QAction *focusToClickSearchAction = nullptr; ClipboardMenu *clipboadMenu = nullptr; }; MainWindow::Private::UI::UI(MainWindow *q) : padWidget(nullptr) { auto mainWidget = new QWidget{q}; auto mainLayout = new QVBoxLayout(mainWidget); mainLayout->setContentsMargins({}); stackWidget = new QStackedWidget{q}; searchTab = new CertificateView{q}; stackWidget->addWidget(searchTab); new KeyCacheOverlay(mainWidget, q); scWidget = new SmartCardWidget{q}; stackWidget->addWidget(scWidget); welcomeWidget = new WelcomeWidget{q}; stackWidget->addWidget(welcomeWidget); mainLayout->addWidget(stackWidget); q->setCentralWidget(mainWidget); } MainWindow::Private::Private(MainWindow *qq) : q(qq) , controller(q) , firstShow(true) , ui(q) { KDAB_SET_OBJECT_NAME(controller); AbstractKeyListModel *flatModel = AbstractKeyListModel::createFlatKeyListModel(q); AbstractKeyListModel *hierarchicalModel = AbstractKeyListModel::createHierarchicalKeyListModel(q); KDAB_SET_OBJECT_NAME(flatModel); KDAB_SET_OBJECT_NAME(hierarchicalModel); #if GPGME_VERSION_NUMBER >= 0x011800 // 1.24.0 auto keyExportDragHandler = std::make_shared(); flatModel->setDragHandler(keyExportDragHandler); hierarchicalModel->setDragHandler(keyExportDragHandler); #endif controller.setFlatModel(flatModel); controller.setHierarchicalModel(hierarchicalModel); controller.setTabWidget(ui.searchTab->tabWidget()); ui.searchTab->tabWidget()->setFlatModel(flatModel); ui.searchTab->tabWidget()->setHierarchicalModel(hierarchicalModel); #ifdef Q_OS_UNIX connect(KWaylandExtras::self(), &KWaylandExtras::windowExported, q, [this](const auto &window, const auto &token) { if (window == q->windowHandle()) { qputenv("PINENTRY_GEOM_HINT", QUrl::toPercentEncoding(token)); } }); q->exportWindow(); #endif setupActions(); ui.stackWidget->setCurrentWidget(ui.searchTab); if (auto action = q->actionCollection()->action(QStringLiteral("view_certificate_overview"))) { action->setChecked(true); } connect(&controller, SIGNAL(contextMenuRequested(QAbstractItemView *, QPoint)), q, SLOT(slotContextMenuRequested(QAbstractItemView *, QPoint))); connect(KeyCache::instance().get(), &KeyCache::keyListingDone, q, [this]() { keyListingDone(); }); q->createGUI(QStringLiteral("kleopatra.rc")); // make toolbar buttons accessible by keyboard auto toolbar = q->findChild(); if (toolbar) { auto toolbarButtons = toolbar->findChildren(); for (auto b : toolbarButtons) { b->setFocusPolicy(Qt::TabFocus); } // move toolbar and its child widgets before the central widget in the tab order; // this is necessary to make Shift+Tab work as expected forceSetTabOrder(q, toolbar); auto toolbarChildren = toolbar->findChildren(); std::for_each(std::rbegin(toolbarChildren), std::rend(toolbarChildren), [toolbar](auto w) { forceSetTabOrder(toolbar, w); }); } if (auto action = q->actionCollection()->action(QStringLiteral("help_whats_this"))) { delete action; } q->setAcceptDrops(true); // set default window size q->resize(QSize(1024, 500)); q->setAutoSaveSettings(); updateSearchBarClickMessage(); updateStatusBar(); if (KeyCache::instance()->initialized()) { keyListingDone(); } // delay setting the models to use the key cache so that the UI (including // the "Loading certificate cache..." overlay) is shown before the // blocking key cache initialization happens QMetaObject::invokeMethod( q, [flatModel, hierarchicalModel]() { flatModel->useKeyCache(true, KeyList::AllKeys); hierarchicalModel->useKeyCache(true, KeyList::AllKeys); }, Qt::QueuedConnection); } MainWindow::Private::~Private() { } MainWindow::MainWindow(QWidget *parent, Qt::WindowFlags flags) : KXmlGuiWindow(parent, flags) , d(new Private(this)) { } MainWindow::~MainWindow() { } void MainWindow::Private::setupActions() { KActionCollection *const coll = q->actionCollection(); const std::vector action_data = { // see keylistcontroller.cpp for more actions // Tools menu #ifndef Q_OS_WIN { "tools_start_kwatchgnupg", i18n("GnuPG Log Viewer"), QString(), "kwatchgnupg", q, [this](bool) { gnupgLogViewer(); }, QString(), }, #endif { "tools_restart_backend", i18nc("@action:inmenu", "Restart Background Processes"), i18nc("@info:tooltip", "Restart the background processes, e.g. after making changes to the configuration."), "view-refresh", q, [this](bool) { restartDaemons(); }, {}, }, // Help menu #ifdef Q_OS_WIN { "help_check_updates", i18n("Check for updates"), QString(), "gpg4win-compact", q, [this](bool) { forceUpdateCheck(); }, QString(), }, #endif // View menu { "view_certificate_overview", i18nc("@action show certificate overview", "Certificates"), i18n("Show certificate overview"), "view-certificate", q, [this](bool) { showCertificateView(); }, QString(), }, { "pad_view", i18nc("@action show input / output area for encrypting/signing resp. decrypting/verifying text", "Notepad"), i18n("Show pad for encrypting/decrypting and signing/verifying text"), "note", q, [this](bool) { showPadView(); }, QString(), }, { "manage_smartcard", i18nc("@action show smartcard management view", "Smartcards"), i18n("Show smartcard management"), "auth-sim-locked", q, [this](bool) { showSmartcardView(); }, QString(), }, // Settings menu { "settings_self_test", i18n("Perform Self-Test"), QString(), nullptr, q, [this](bool) { selfTest(); }, QString(), }, { "configure_groups", i18n("Configure Groups..."), QString(), "group", q, [this](bool) { configureGroups(); }, QString(), }, // Toolbar { "configure_groups_toolbar", i18nc("@action:intoolbar", "Groups"), QString(), "group", q, [this](bool) { configureGroups(); }, QString(), }}; make_actions_from_data(action_data, coll); if (!Settings().groupsEnabled()) { if (auto action = coll->action(QStringLiteral("configure_groups"))) { delete action; } } for (const QString &name : mainViewActionNames) { if (auto action = coll->action(name)) { action->setCheckable(true); } } KStandardAction::close(q, SLOT(close()), coll); KStandardAction::quit(q, SLOT(closeAndQuit()), coll); KStandardAction::configureToolbars(q, SLOT(configureToolbars()), coll); KStandardAction::keyBindings(q, SLOT(editKeybindings()), coll); KStandardAction::preferences(qApp, SLOT(openOrRaiseConfigDialog()), coll); focusToClickSearchAction = new QAction(i18n("Set Focus to Quick Search"), q); coll->addAction(QStringLiteral("focus_to_quickseach"), focusToClickSearchAction); coll->setDefaultShortcut(focusToClickSearchAction, QKeySequence(Qt::ALT | Qt::Key_Q)); connect(focusToClickSearchAction, SIGNAL(triggered(bool)), q, SLOT(slotFocusQuickSearch())); clipboadMenu = new ClipboardMenu(q); clipboadMenu->setMainWindow(q); clipboadMenu->clipboardMenu()->setIcon(QIcon::fromTheme(QStringLiteral("edit-paste"))); clipboadMenu->clipboardMenu()->setPopupMode(QToolButton::InstantPopup); coll->addAction(QStringLiteral("clipboard_menu"), clipboadMenu->clipboardMenu()); /* Add additional help actions for documentation */ const auto compendium = new DocAction(QIcon::fromTheme(QStringLiteral("gpg4win-compact")), i18n("Gpg4win Compendium"), i18nc("The Gpg4win compendium is only available" "at this point (24.7.2017) in german and english." "Please check with Gpg4win before translating this filename.", "gpg4win-compendium-en.pdf"), QStringLiteral("../share/gpg4win"), coll); coll->addAction(QStringLiteral("help_doc_compendium"), compendium); /* Documentation centered around the german approved VS-NfD mode for official * RESTRICTED communication. This is only available in some distributions with * the focus on official communications. */ const auto quickguide = new DocAction(QIcon::fromTheme(QStringLiteral("help-contextual")), i18n("&Quickguide"), i18nc("Only available in German and English. Leave to English for other languages.", "encrypt_and_sign_gnupgvsd_en.pdf"), QStringLiteral("../share/doc/gnupg-vsd"), coll); coll->addAction(QStringLiteral("help_doc_quickguide"), quickguide); const auto symguide = new DocAction(QIcon::fromTheme(QStringLiteral("help-contextual")), i18n("&Password-based encryption"), i18nc("Only available in German and English. Leave to English for other languages.", "symmetric_encryption_gnupgvsd_en.pdf"), QStringLiteral("../share/doc/gnupg-vsd"), coll); coll->addAction(QStringLiteral("help_doc_symenc"), symguide); const auto groups = new DocAction(QIcon::fromTheme(QStringLiteral("help-contextual")), i18n("&Group configuration"), i18nc("Only available in German and English. Leave to English for other languages.", "groupfeature_gnupgvsd_en.pdf"), QStringLiteral("../share/doc/gnupg-vsd"), coll); coll->addAction(QStringLiteral("help_doc_groups"), groups); #ifdef Q_OS_WIN const auto gpgol = new DocAction(QIcon::fromTheme(QStringLiteral("help-contextual")), i18n("&Mail encryption in Outlook"), i18nc("Only available in German and English. Leave to English for other languages. Only shown on Windows.", "gpgol_outlook_addin_en.pdf"), QStringLiteral("../share/doc/gnupg-vsd"), coll); coll->addAction(QStringLiteral("help_doc_gpgol"), gpgol); #endif /* The submenu with advanced topics */ const auto certmngmnt = new DocAction(QIcon::fromTheme(QStringLiteral("help-contextual")), i18n("&Certification Management"), i18nc("Only available in German and English. Leave to English for other languages.", "certification_management_gnupgvsd_en.pdf"), QStringLiteral("../share/doc/gnupg-vsd"), coll); coll->addAction(QStringLiteral("help_doc_cert_management"), certmngmnt); const auto smartcard = new DocAction(QIcon::fromTheme(QStringLiteral("help-contextual")), i18n("&Smartcard setup"), i18nc("Only available in German and English. Leave to English for other languages.", "smartcard_setup_gnupgvsd_en.pdf"), QStringLiteral("../share/doc/gnupg-vsd"), coll); coll->addAction(QStringLiteral("help_doc_smartcard"), smartcard); const auto man_gnupg = new DocAction(QIcon::fromTheme(QStringLiteral("help-contextual")), i18n("GnuPG Command&line"), QStringLiteral("gnupg_manual_en.pdf"), QStringLiteral("../share/doc/gnupg-vsd"), coll); coll->addAction(QStringLiteral("help_doc_gnupg"), man_gnupg); /* The secops */ const auto approvalmanual = new DocAction(QIcon::fromTheme(QStringLiteral("dvipdf")), i18n("Manual for VS-NfD approval (German)"), i18nc("Only available in German. Keep German file name for all languages", "Handbuch-Zulassung-gnupgvsd-v3.2.pdf"), QStringLiteral("../share/doc/gnupg-vsd"), coll); coll->addAction(QStringLiteral("help_doc_approval_manual"), approvalmanual); const auto vsa10573 = new DocAction(QIcon::fromTheme(QStringLiteral("dvipdf")), i18n("SecOps VSA-10573"), i18nc("Only available in German and English. Leave to English for other languages.", "BSI-VSA-10573-ENG_secops-20220207.pdf"), QStringLiteral("../share/doc/gnupg-vsd"), coll); coll->addAction(QStringLiteral("help_doc_vsa10573"), vsa10573); const auto vsa10584 = new DocAction(QIcon::fromTheme(QStringLiteral("dvipdf")), i18n("SecOps VSA-10584"), i18nc("Only available in German and English. Leave to English for other languages.", "BSI-VSA-10584-ENG_secops-20220207.pdf"), QStringLiteral("../share/doc/gnupg-vsd"), coll); coll->addAction(QStringLiteral("help_doc_vsa10584"), vsa10584); q->setStandardToolBarMenuEnabled(true); controller.createActions(coll); ui.searchTab->tabWidget()->createActions(coll); } void MainWindow::Private::slotConfigCommitted() { controller.updateConfig(); updateStatusBar(); } void MainWindow::closeEvent(QCloseEvent *e) { // KMainWindow::closeEvent() insists on quitting the application, // so do not let it touch the event... qCDebug(KLEOPATRA_LOG); if (d->controller.hasRunningCommands()) { if (d->controller.shutdownWarningRequired()) { const int ret = KMessageBox::warningContinueCancel(this, i18n("There are still some background operations ongoing. " "These will be terminated when closing the window. " "Proceed?"), i18n("Ongoing Background Tasks")); if (ret != KMessageBox::Continue) { e->ignore(); return; } } d->controller.cancelCommands(); if (d->controller.hasRunningCommands()) { // wait for them to be finished: setEnabled(false); QEventLoop ev; QTimer::singleShot(100ms, &ev, &QEventLoop::quit); connect(&d->controller, &KeyListController::commandsExecuting, &ev, &QEventLoop::quit); ev.exec(); if (d->controller.hasRunningCommands()) qCWarning(KLEOPATRA_LOG) << "controller still has commands running, this may crash now..."; setEnabled(true); } } unexportWindow(); if (isQuitting || qApp->isSavingSession()) { d->ui.searchTab->tabWidget()->saveViews(); KConfigGroup grp(KConfigGroup(KSharedConfig::openConfig(), autoSaveGroup())); saveMainWindowSettings(grp); e->accept(); } else { e->ignore(); hide(); } } void MainWindow::showEvent(QShowEvent *e) { KXmlGuiWindow::showEvent(e); if (d->firstShow) { d->ui.searchTab->tabWidget()->loadViews(KSharedConfig::openStateConfig(), QStringLiteral("KeyList")); d->firstShow = false; } if (!savedGeometry.isEmpty()) { restoreGeometry(savedGeometry); } } void MainWindow::hideEvent(QHideEvent *e) { savedGeometry = saveGeometry(); KXmlGuiWindow::hideEvent(e); } void MainWindow::importCertificatesFromFile(const QStringList &files) { if (!files.empty()) { d->createAndStart(files); } } static QStringList extract_local_files(const QMimeData *data) { const QList urls = data->urls(); // begin workaround KDE/Qt misinterpretation of text/uri-list QList::const_iterator end = urls.end(); if (urls.size() > 1 && !urls.back().isValid()) { --end; } // end workaround QStringList result; std::transform(urls.begin(), end, std::back_inserter(result), std::mem_fn(&QUrl::toLocalFile)); result.erase(std::remove_if(result.begin(), result.end(), std::mem_fn(&QString::isEmpty)), result.end()); return result; } static bool can_decode_local_files(const QMimeData *data) { if (!data) { return false; } return !extract_local_files(data).empty(); } void MainWindow::dragEnterEvent(QDragEnterEvent *e) { qCDebug(KLEOPATRA_LOG); if (can_decode_local_files(e->mimeData())) { e->acceptProposedAction(); } } void MainWindow::dropEvent(QDropEvent *e) { qCDebug(KLEOPATRA_LOG); if (!can_decode_local_files(e->mimeData())) { return; } e->setDropAction(Qt::CopyAction); const QStringList files = extract_local_files(e->mimeData()); KleopatraApplication::instance()->handleFiles(files); e->accept(); } void MainWindow::readProperties(const KConfigGroup &cg) { qCDebug(KLEOPATRA_LOG); KXmlGuiWindow::readProperties(cg); setHidden(cg.readEntry("hidden", false)); } void MainWindow::saveProperties(KConfigGroup &cg) { qCDebug(KLEOPATRA_LOG); KXmlGuiWindow::saveProperties(cg); cg.writeEntry("hidden", isHidden()); } void MainWindow::exportWindow() { #ifdef Q_OS_UNIX (void)winId(); // Ensures that windowHandle() returns the window KWaylandExtras::self()->exportWindow(windowHandle()); #endif } void MainWindow::unexportWindow() { #ifdef Q_OS_UNIX KWaylandExtras::self()->unexportWindow(windowHandle()); #endif } +KeyListController *MainWindow::keyListController() +{ + return &d->controller; +} + #include "mainwindow.moc" #include "moc_mainwindow.cpp" diff --git a/src/mainwindow.h b/src/mainwindow.h index 7d336c449..dabf46b2b 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -1,52 +1,59 @@ /* -*- mode: c++; c-basic-offset:4 -*- mainwindow.h This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2016 Bundesamt für Sicherheit in der Informationstechnik SPDX-FileContributor: Intevation GmbH SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include #include +namespace Kleo +{ +class KeyListController; +} + class MainWindow : public KXmlGuiWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = nullptr, Qt::WindowFlags f = {}); ~MainWindow() override; + Kleo::KeyListController *keyListController(); + public Q_SLOTS: void importCertificatesFromFile(const QStringList &files); void exportWindow(); void unexportWindow(); protected: QByteArray savedGeometry; void closeEvent(QCloseEvent *e) override; void showEvent(QShowEvent *e) override; void hideEvent(QHideEvent *e) override; void dragEnterEvent(QDragEnterEvent *) override; void dropEvent(QDropEvent *) override; void readProperties(const KConfigGroup &cg) override; void saveProperties(KConfigGroup &cg) override; private: class Private; kdtools::pimpl_ptr d; Q_PRIVATE_SLOT(d, void closeAndQuit()) Q_PRIVATE_SLOT(d, void configureToolbars()) Q_PRIVATE_SLOT(d, void editKeybindings()) Q_PRIVATE_SLOT(d, void slotConfigCommitted()) Q_PRIVATE_SLOT(d, void slotContextMenuRequested(QAbstractItemView *, QPoint)) Q_PRIVATE_SLOT(d, void slotFocusQuickSearch()) Q_PRIVATE_SLOT(d, void showPadView()) };