diff --git a/src/kleo/dn.cpp b/src/kleo/dn.cpp index 18b8644cb..fdd1d7b8b 100644 --- a/src/kleo/dn.cpp +++ b/src/kleo/dn.cpp @@ -1,584 +1,583 @@ /* dn.cpp This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2004 Klarälvdalens Datakonsult AB DN parsing: SPDX-FileCopyrightText: 2002 g 10 Code GmbH SPDX-FileCopyrightText: 2004 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #include "dn.h" #include "oidmap.h" #include "ui/dnattributeorderconfigwidget.h" #include #include -#include #include #include #include #include #include #include #include #include #include #ifdef _MSC_VER #define strcasecmp _stricmp #endif class Kleo::DN::Private { public: Private() : mRefCount(0) {} Private(const Private &other) : attributes(other.attributes), reorderedAttributes(other.reorderedAttributes), mRefCount(0) { } int ref() { return ++mRefCount; } int unref() { if (--mRefCount <= 0) { delete this; return 0; } else { return mRefCount; } } int refCount() const { return mRefCount; } DN::Attribute::List attributes; DN::Attribute::List reorderedAttributes; private: int mRefCount; }; namespace { struct DnPair { char *key; char *value; }; } // copied from CryptPlug and adapted to work on DN::Attribute::List: #define digitp(p) (*(p) >= '0' && *(p) <= '9') #define hexdigitp(a) (digitp (a) \ || (*(a) >= 'A' && *(a) <= 'F') \ || (*(a) >= 'a' && *(a) <= 'f')) #define xtoi_1(p) (*(p) <= '9'? (*(p)- '0'): \ *(p) <= 'F'? (*(p)-'A'+10):(*(p)-'a'+10)) #define xtoi_2(p) ((xtoi_1(p) * 16) + xtoi_1((p)+1)) static char * trim_trailing_spaces(char *string) { char *p, *mark; for (mark = nullptr, p = string; *p; p++) { if (isspace(*p)) { if (!mark) { mark = p; } } else { mark = nullptr; } } if (mark) { *mark = '\0'; } return string; } /* Parse a DN and return an array-ized one. This is not a validating parser and it does not support any old-stylish syntax; gpgme is expected to return only rfc2253 compatible strings. */ static const unsigned char * parse_dn_part(DnPair *array, const unsigned char *string) { const unsigned char *s, *s1; size_t n; char *p; /* parse attributeType */ for (s = string + 1; *s && *s != '='; s++) ; if (!*s) { return nullptr; /* error */ } n = s - string; if (!n) { return nullptr; /* empty key */ } p = (char *)malloc(n + 1); memcpy(p, string, n); p[n] = 0; trim_trailing_spaces((char *)p); // map OIDs to their names: for (unsigned int i = 0; i < numOidMaps; ++i) if (!strcasecmp((char *)p, oidmap[i].oid)) { free(p); p = strdup(oidmap[i].name); break; } array->key = p; string = s + 1; if (*string == '#') { /* hexstring */ string++; for (s = string; hexdigitp(s); s++) { s++; } n = s - string; if (!n || (n & 1)) { return nullptr; /* empty or odd number of digits */ } n /= 2; array->value = p = (char *)malloc(n + 1); for (s1 = string; n; s1 += 2, n--) { *p++ = xtoi_2(s1); } *p = 0; } else { /* regular v3 quoted string */ for (n = 0, s = string; *s; s++) { if (*s == '\\') { /* pair */ s++; if (*s == ',' || *s == '=' || *s == '+' || *s == '<' || *s == '>' || *s == '#' || *s == ';' || *s == '\\' || *s == '\"' || *s == ' ') { n++; } else if (hexdigitp(s) && hexdigitp(s + 1)) { s++; n++; } else { return nullptr; /* invalid escape sequence */ } } else if (*s == '\"') { return nullptr; /* invalid encoding */ } else if (*s == ',' || *s == '=' || *s == '+' || *s == '<' || *s == '>' || *s == '#' || *s == ';') { break; } else { n++; } } array->value = p = (char *)malloc(n + 1); for (s = string; n; s++, n--) { if (*s == '\\') { s++; if (hexdigitp(s)) { *p++ = xtoi_2(s); s++; } else { *p++ = *s; } } else { *p++ = *s; } } *p = 0; } return s; } /* Parse a DN and return an array-ized one. This is not a validating parser and it does not support any old-stylish syntax; gpgme is expected to return only rfc2253 compatible strings. */ static Kleo::DN::Attribute::List parse_dn(const unsigned char *string) { if (!string) { return QVector(); } QVector result; while (*string) { while (*string == ' ') { string++; } if (!*string) { break; /* ready */ } DnPair pair = { nullptr, nullptr }; string = parse_dn_part(&pair, string); if (!string) { goto failure; } if (pair.key && pair.value) result.push_back(Kleo::DN::Attribute(QString::fromUtf8(pair.key), QString::fromUtf8(pair.value))); free(pair.key); free(pair.value); while (*string == ' ') { string++; } if (*string && *string != ',' && *string != ';' && *string != '+') { goto failure; /* invalid delimiter */ } if (*string) { string++; } } return result; failure: return QVector(); } static QVector parse_dn(const QString &dn) { return parse_dn((const unsigned char *)dn.toUtf8().data()); } static QString dn_escape(const QString &s) { QString result; for (int i = 0, end = s.length(); i != end; ++i) { const QChar ch = s[i]; switch (ch.unicode()) { case ',': case '+': case '"': case '\\': case '<': case '>': case ';': result += QLatin1Char('\\'); // fall through Q_FALLTHROUGH(); default: result += ch; } } return result; } static QString serialise(const QVector &dn, const QString &sep) { QStringList result; for (QVector::const_iterator it = dn.begin(); it != dn.end(); ++it) if (!(*it).name().isEmpty() && !(*it).value().isEmpty()) { result.push_back((*it).name().trimmed() + QLatin1Char('=') + dn_escape((*it).value().trimmed())); } return result.join(sep); } static Kleo::DN::Attribute::List reorder_dn(const Kleo::DN::Attribute::List &dn) { const QStringList &attrOrder = Kleo::DNAttributeMapper::instance()->attributeOrder(); Kleo::DN::Attribute::List unknownEntries; Kleo::DN::Attribute::List result; unknownEntries.reserve(dn.size()); result.reserve(dn.size()); // find all unknown entries in their order of appearance for (Kleo::DN::const_iterator it = dn.begin(); it != dn.end(); ++it) if (!attrOrder.contains((*it).name())) { unknownEntries.push_back(*it); } // process the known attrs in the desired order for (QStringList::const_iterator oit = attrOrder.begin(); oit != attrOrder.end(); ++oit) if (*oit == QLatin1String("_X_")) { // insert the unknown attrs std::copy(unknownEntries.begin(), unknownEntries.end(), std::back_inserter(result)); unknownEntries.clear(); // don't produce dup's } else { for (Kleo::DN::const_iterator dnit = dn.begin(); dnit != dn.end(); ++dnit) if ((*dnit).name() == *oit) { result.push_back(*dnit); } } return result; } // // // class DN // // Kleo::DN::DN() { d = new Private(); d->ref(); } Kleo::DN::DN(const QString &dn) { d = new Private(); d->ref(); d->attributes = parse_dn(dn); } Kleo::DN::DN(const char *utf8DN) { d = new Private(); d->ref(); if (utf8DN) { d->attributes = parse_dn((const unsigned char *)utf8DN); } } Kleo::DN::DN(const DN &other) : d(other.d) { if (d) { d->ref(); } } Kleo::DN::~DN() { if (d) { d->unref(); } } const Kleo::DN &Kleo::DN::operator=(const DN &that) { if (this->d == that.d) { return *this; } if (that.d) { that.d->ref(); } if (this->d) { this->d->unref(); } this->d = that.d; return *this; } QString Kleo::DN::prettyDN() const { if (!d) { return QString(); } if (d->reorderedAttributes.empty()) { d->reorderedAttributes = reorder_dn(d->attributes); } return serialise(d->reorderedAttributes, QStringLiteral(",")); } QString Kleo::DN::dn() const { return d ? serialise(d->attributes, QStringLiteral(",")) : QString(); } QString Kleo::DN::dn(const QString &sep) const { return d ? serialise(d->attributes, sep) : QString(); } // static QString Kleo::DN::escape(const QString &value) { return dn_escape(value); } void Kleo::DN::detach() { if (!d) { d = new Kleo::DN::Private(); d->ref(); } else if (d->refCount() > 1) { Kleo::DN::Private *d_save = d; d = new Kleo::DN::Private(*d); d->ref(); d_save->unref(); } } void Kleo::DN::append(const Attribute &attr) { detach(); d->attributes.push_back(attr); d->reorderedAttributes.clear(); } QString Kleo::DN::operator[](const QString &attr) const { if (!d) { return QString(); } const QString attrUpper = attr.toUpper(); for (QVector::const_iterator it = d->attributes.constBegin(); it != d->attributes.constEnd(); ++it) if ((*it).name() == attrUpper) { return (*it).value(); } return QString(); } static QVector empty; Kleo::DN::const_iterator Kleo::DN::begin() const { return d ? d->attributes.constBegin() : empty.constBegin(); } Kleo::DN::const_iterator Kleo::DN::end() const { return d ? d->attributes.constEnd() : empty.constEnd(); } ///////////////////// namespace { struct ltstr { bool operator()(const char *s1, const char *s2) const { return qstrcmp(s1, s2) < 0; } }; } static const QStringList defaultOrder = { QStringLiteral("CN"), QStringLiteral("L"), QStringLiteral("_X_"), QStringLiteral("OU"), QStringLiteral("O"), QStringLiteral("C"), }; static std::pair const attributeLabels[] = { #define MAKE_PAIR(x,y) std::pair( x, y ) MAKE_PAIR("CN", I18N_NOOP("Common name")), MAKE_PAIR("SN", I18N_NOOP("Surname")), MAKE_PAIR("GN", I18N_NOOP("Given name")), MAKE_PAIR("L", I18N_NOOP("Location")), MAKE_PAIR("T", I18N_NOOP("Title")), MAKE_PAIR("OU", I18N_NOOP("Organizational unit")), MAKE_PAIR("O", I18N_NOOP("Organization")), MAKE_PAIR("PC", I18N_NOOP("Postal code")), MAKE_PAIR("C", I18N_NOOP("Country code")), MAKE_PAIR("SP", I18N_NOOP("State or province")), MAKE_PAIR("DC", I18N_NOOP("Domain component")), MAKE_PAIR("BC", I18N_NOOP("Business category")), MAKE_PAIR("EMAIL", I18N_NOOP("Email address")), MAKE_PAIR("MAIL", I18N_NOOP("Mail address")), MAKE_PAIR("MOBILE", I18N_NOOP("Mobile phone number")), MAKE_PAIR("TEL", I18N_NOOP("Telephone number")), MAKE_PAIR("FAX", I18N_NOOP("Fax number")), MAKE_PAIR("STREET", I18N_NOOP("Street address")), MAKE_PAIR("UID", I18N_NOOP("Unique ID")) #undef MAKE_PAIR }; static const unsigned int numAttributeLabels = sizeof attributeLabels / sizeof * attributeLabels; class Kleo::DNAttributeMapper::Private { public: Private(); std::map map; QStringList attributeOrder; }; Kleo::DNAttributeMapper::Private::Private() : map(attributeLabels, attributeLabels + numAttributeLabels) {} Kleo::DNAttributeMapper::DNAttributeMapper() { d = new Private(); const KConfigGroup config(KSharedConfig::openConfig(), "DN"); d->attributeOrder = config.readEntry("AttributeOrder", defaultOrder); mSelf = this; } Kleo::DNAttributeMapper::~DNAttributeMapper() { mSelf = nullptr; delete d; d = nullptr; } Kleo::DNAttributeMapper *Kleo::DNAttributeMapper::mSelf = nullptr; const Kleo::DNAttributeMapper *Kleo::DNAttributeMapper::instance() { if (!mSelf) { (void)new DNAttributeMapper(); } return mSelf; } QString Kleo::DNAttributeMapper::name2label(const QString &s) const { const std::map::const_iterator it = d->map.find(s.trimmed().toUpper().toLatin1().constData()); if (it == d->map.end()) { return QString(); } return i18n(it->second); } QStringList Kleo::DNAttributeMapper::names() const { QStringList result; for (std::map::const_iterator it = d->map.begin(); it != d->map.end(); ++it) { result.push_back(QLatin1String(it->first)); } return result; } const QStringList &Kleo::DNAttributeMapper::attributeOrder() const { return d->attributeOrder; } void Kleo::DNAttributeMapper::setAttributeOrder(const QStringList &order) { d->attributeOrder = order.empty() ? defaultOrder : order; KConfigGroup config(KSharedConfig::openConfig(), "DN"); config.writeEntry("AttributeOrder", order); } Kleo::DNAttributeOrderConfigWidget *Kleo::DNAttributeMapper::configWidget(QWidget *parent) const { return new DNAttributeOrderConfigWidget(mSelf, parent); } diff --git a/src/kleo/enum.cpp b/src/kleo/enum.cpp index 10bbcdb33..1d33e8023 100644 --- a/src/kleo/enum.cpp +++ b/src/kleo/enum.cpp @@ -1,325 +1,324 @@ /* kleo/enum.cpp This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2004 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #include "enum.h" #include "libkleo_debug.h" #include "models/keycache.h" #include #include #include #include #include -#include #include static const struct { Kleo::CryptoMessageFormat format; const char *displayName; const char *configName; } cryptoMessageFormats[] = { { Kleo::InlineOpenPGPFormat, I18N_NOOP("Inline OpenPGP (deprecated)"), "inline openpgp" }, { Kleo::OpenPGPMIMEFormat, I18N_NOOP("OpenPGP/MIME"), "openpgp/mime" }, { Kleo::SMIMEFormat, I18N_NOOP("S/MIME"), "s/mime" }, { Kleo::SMIMEOpaqueFormat, I18N_NOOP("S/MIME Opaque"), "s/mime opaque" }, { Kleo::AnySMIME, I18N_NOOP("Any S/MIME"), "any s/mime" }, { Kleo::AnyOpenPGP, I18N_NOOP("Any OpenPGP"), "any openpgp" }, }; static const unsigned int numCryptoMessageFormats = sizeof cryptoMessageFormats / sizeof * cryptoMessageFormats; const char *Kleo::cryptoMessageFormatToString(Kleo::CryptoMessageFormat f) { if (f == AutoFormat) { return "auto"; } for (unsigned int i = 0; i < numCryptoMessageFormats; ++i) if (f == cryptoMessageFormats[i].format) { return cryptoMessageFormats[i].configName; } return nullptr; } QStringList Kleo::cryptoMessageFormatsToStringList(unsigned int f) { QStringList result; for (unsigned int i = 0; i < numCryptoMessageFormats; ++i) if (f & cryptoMessageFormats[i].format) { result.push_back(QLatin1String(cryptoMessageFormats[i].configName)); } return result; } QString Kleo::cryptoMessageFormatToLabel(Kleo::CryptoMessageFormat f) { if (f == AutoFormat) { return i18n("Any"); } for (unsigned int i = 0; i < numCryptoMessageFormats; ++i) if (f == cryptoMessageFormats[i].format) { return i18n(cryptoMessageFormats[i].displayName); } return QString(); } Kleo::CryptoMessageFormat Kleo::stringToCryptoMessageFormat(const QString &s) { const QString t = s.toLower(); for (unsigned int i = 0; i < numCryptoMessageFormats; ++i) if (t == QLatin1String(cryptoMessageFormats[i].configName)) { return cryptoMessageFormats[i].format; } return AutoFormat; } unsigned int Kleo::stringListToCryptoMessageFormats(const QStringList &sl) { unsigned int result = 0; for (QStringList::const_iterator it = sl.begin(); it != sl.end(); ++it) { result |= stringToCryptoMessageFormat(*it); } return result; } // For the config values used below, see also kaddressbook/editors/cryptowidget.cpp const char *Kleo::encryptionPreferenceToString(EncryptionPreference pref) { switch (pref) { case UnknownPreference: return nullptr; case NeverEncrypt: return "never"; case AlwaysEncrypt: return "always"; case AlwaysEncryptIfPossible: return "alwaysIfPossible"; case AlwaysAskForEncryption: return "askAlways"; case AskWheneverPossible: return "askWhenPossible"; } return nullptr; // keep the compiler happy } Kleo::EncryptionPreference Kleo::stringToEncryptionPreference(const QString &str) { if (str == QLatin1String("never")) { return NeverEncrypt; } if (str == QLatin1String("always")) { return AlwaysEncrypt; } if (str == QLatin1String("alwaysIfPossible")) { return AlwaysEncryptIfPossible; } if (str == QLatin1String("askAlways")) { return AlwaysAskForEncryption; } if (str == QLatin1String("askWhenPossible")) { return AskWheneverPossible; } return UnknownPreference; } QString Kleo::encryptionPreferenceToLabel(EncryptionPreference pref) { switch (pref) { case NeverEncrypt: return i18n("Never Encrypt"); case AlwaysEncrypt: return i18n("Always Encrypt"); case AlwaysEncryptIfPossible: return i18n("Always Encrypt If Possible"); case AlwaysAskForEncryption: return i18n("Ask"); case AskWheneverPossible: return i18n("Ask Whenever Possible"); default: return xi18nc("no specific preference", "none"); } } const char *Kleo::signingPreferenceToString(SigningPreference pref) { switch (pref) { case UnknownSigningPreference: return nullptr; case NeverSign: return "never"; case AlwaysSign: return "always"; case AlwaysSignIfPossible: return "alwaysIfPossible"; case AlwaysAskForSigning: return "askAlways"; case AskSigningWheneverPossible: return "askWhenPossible"; } return nullptr; // keep the compiler happy } Kleo::SigningPreference Kleo::stringToSigningPreference(const QString &str) { if (str == QLatin1String("never")) { return NeverSign; } if (str == QLatin1String("always")) { return AlwaysSign; } if (str == QLatin1String("alwaysIfPossible")) { return AlwaysSignIfPossible; } if (str == QLatin1String("askAlways")) { return AlwaysAskForSigning; } if (str == QLatin1String("askWhenPossible")) { return AskSigningWheneverPossible; } return UnknownSigningPreference; } QString Kleo::signingPreferenceToLabel(SigningPreference pref) { switch (pref) { case NeverSign: return i18n("Never Sign"); case AlwaysSign: return i18n("Always Sign"); case AlwaysSignIfPossible: return i18n("Always Sign If Possible"); case AlwaysAskForSigning: return i18n("Ask"); case AskSigningWheneverPossible: return i18n("Ask Whenever Possible"); default: return i18nc("no specific preference", ""); } } Kleo::TrustLevel Kleo::trustLevel(const GpgME::Key &key) { TrustLevel maxTl = Level0; for (int i = 0, c = key.numUserIDs(); i < c; ++i) { const auto tl = trustLevel(key.userID(i)); maxTl = qMax(maxTl, tl); if (maxTl == Level4) { break; } } return maxTl; } namespace { bool hasTrustedSignature(const GpgME::UserID &uid) { // lazily intialized cache static std::shared_ptr keyCache; if (!keyCache) { keyCache = Kleo::KeyCache::instance(); } if (!keyCache->initialized()) { QEventLoop el; QObject::connect(keyCache.get(), &Kleo::KeyCache::keyListingDone, &el, &QEventLoop::quit); el.exec(); } const auto signatures = uid.signatures(); std::vector sigKeyIDs; std::transform(signatures.cbegin(), signatures.cend(), std::back_inserter(sigKeyIDs), std::bind(&GpgME::UserID::Signature::signerKeyID, std::placeholders::_1)); const auto keys = keyCache->findByKeyIDOrFingerprint(sigKeyIDs); return std::any_of(keys.cbegin(), keys.cend(), [](const GpgME::Key &key) { return key.ownerTrust() == GpgME::Key::Ultimate; }); } } Kleo::TrustLevel Kleo::trustLevel(const GpgME::UserID &uid) { // Modelled after https://wiki.gnupg.org/EasyGpg2016/AutomatedEncryption, // but modified to cover all cases, unlike the pseudocode in the document. // // TODO: Check whether the key comes from a trusted source (Cert/PKA/DANE/WKD) switch (uid.validity()) { case GpgME::UserID::Unknown: case GpgME::UserID::Undefined: case GpgME::UserID::Never: // Not enough trust -> level 0 return Level0; case GpgME::UserID::Marginal: // Marginal trust without TOFU data means the key is still trusted // through the Web of Trust -> level 2 if (uid.tofuInfo().isNull()) { return Level2; } // Marginal trust with TOFU, level will depend on TOFU history switch (uid.tofuInfo().validity()) { case GpgME::TofuInfo::ValidityUnknown: case GpgME::TofuInfo::Conflict: case GpgME::TofuInfo::NoHistory: // Marginal trust, but not enough history -> level 0 return Level0; case GpgME::TofuInfo::LittleHistory: // Marginal trust, but too little history -> level 1 return Level1; case GpgME::TofuInfo::BasicHistory: case GpgME::TofuInfo::LargeHistory: // Marginal trust and enough history -> level 2 return Level2; } return Level2; // Not reached, but avoids fallthrough warnings case GpgME::UserID::Full: // Full trust, trust level depends whether the UserID is signed with // at least one key with Ultimate ownertrust. return hasTrustedSignature(uid) ? Level4 : Level3; case GpgME::UserID::Ultimate: // Ultimate trust -> leve 4 return Level4; } Q_UNREACHABLE(); } diff --git a/src/models/keylistmodel.cpp b/src/models/keylistmodel.cpp index c594febc7..5c851fa04 100644 --- a/src/models/keylistmodel.cpp +++ b/src/models/keylistmodel.cpp @@ -1,1393 +1,1392 @@ /* -*- mode: c++; c-basic-offset:4 -*- models/keylistmodel.cpp This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include "keylistmodel.h" #include "keycache.h" -#include "keylist.h" #include "kleo/keygroup.h" #include "kleo/predicates.h" #include "kleo/keyfiltermanager.h" #include "kleo/keyfilter.h" #include "utils/algorithm.h" #include "utils/formatting.h" #ifdef KLEO_MODEL_TEST # include #endif #include #include #include #include #include #include #include #include #include #ifndef Q_MOC_RUN // QTBUG-22829 #include #include #endif #include #include #include #include #include #if GPGMEPP_VERSION >= 0x10E00 // 1.14.0 # define GPGME_HAS_REMARKS #endif using namespace GpgME; using namespace Kleo; using namespace Kleo::KeyList; Q_DECLARE_METATYPE(GpgME::Key) Q_DECLARE_METATYPE(KeyGroup) class AbstractKeyListModel::Private { AbstractKeyListModel *const q; public: explicit Private(AbstractKeyListModel *qq); void updateFromKeyCache(); public: int m_toolTipOptions = Formatting::Validity; mutable QHash prettyEMailCache; mutable QHash remarksCache; bool m_useKeyCache = false; KeyList::Options m_keyListOptions = AllKeys; std::vector m_remarkKeys; }; AbstractKeyListModel::Private::Private(Kleo::AbstractKeyListModel *qq) : q(qq) { } void AbstractKeyListModel::Private::updateFromKeyCache() { if (m_useKeyCache) { q->setKeys(m_keyListOptions == SecretKeysOnly ? KeyCache::instance()->secretKeys() : KeyCache::instance()->keys()); if (m_keyListOptions == IncludeGroups) { q->setGroups(KeyCache::instance()->groups()); } } } AbstractKeyListModel::AbstractKeyListModel(QObject *p) : QAbstractItemModel(p), KeyListModelInterface(), d(new Private(this)) { } AbstractKeyListModel::~AbstractKeyListModel() {} void AbstractKeyListModel::setToolTipOptions(int opts) { d->m_toolTipOptions = opts; } int AbstractKeyListModel::toolTipOptions() const { return d->m_toolTipOptions; } void AbstractKeyListModel::setRemarkKeys(const std::vector &keys) { d->m_remarkKeys = keys; } std::vector AbstractKeyListModel::remarkKeys() const { return d->m_remarkKeys; } Key AbstractKeyListModel::key(const QModelIndex &idx) const { if (idx.isValid()) { return doMapToKey(idx); } else { return Key::null; } } std::vector AbstractKeyListModel::keys(const QList &indexes) const { std::vector result; result.reserve(indexes.size()); std::transform(indexes.begin(), indexes.end(), std::back_inserter(result), [this](const QModelIndex &idx) { return this->key(idx); }); result.erase(std::remove_if(result.begin(), result.end(), std::mem_fn(&GpgME::Key::isNull)), result.end()); _detail::remove_duplicates_by_fpr(result); return result; } KeyGroup AbstractKeyListModel::group(const QModelIndex &idx) const { if (idx.isValid()) { return doMapToGroup(idx); } else { return KeyGroup(); } } QModelIndex AbstractKeyListModel::index(const Key &key) const { return index(key, 0); } QModelIndex AbstractKeyListModel::index(const Key &key, int col) const { if (key.isNull() || col < 0 || col >= NumColumns) { return {}; } else { return doMapFromKey(key, col); } } QList AbstractKeyListModel::indexes(const std::vector &keys) const { QList result; result.reserve(keys.size()); std::transform(keys.begin(), keys.end(), std::back_inserter(result), [this](const Key &key) { return this->index(key); }); return result; } QModelIndex AbstractKeyListModel::index(const KeyGroup &group) const { return index(group, 0); } QModelIndex AbstractKeyListModel::index(const KeyGroup &group, int col) const { if (group.isNull() || col < 0 || col >= NumColumns) { return {}; } else { return doMapFromGroup(group, col); } } void AbstractKeyListModel::setKeys(const std::vector &keys) { clear(Keys); addKeys(keys); } QModelIndex AbstractKeyListModel::addKey(const Key &key) { const std::vector vec(1, key); const QList l = doAddKeys(vec); return l.empty() ? QModelIndex() : l.front(); } void AbstractKeyListModel::removeKey(const Key &key) { if (key.isNull()) { return; } doRemoveKey(key); d->prettyEMailCache.remove(key.primaryFingerprint()); d->remarksCache.remove(key.primaryFingerprint()); } QList AbstractKeyListModel::addKeys(const std::vector &keys) { std::vector sorted; sorted.reserve(keys.size()); std::remove_copy_if(keys.begin(), keys.end(), std::back_inserter(sorted), std::mem_fn(&Key::isNull)); std::sort(sorted.begin(), sorted.end(), _detail::ByFingerprint()); return doAddKeys(sorted); } void AbstractKeyListModel::setGroups(const std::vector &groups) { clear(Groups); doSetGroups(groups); } QModelIndex AbstractKeyListModel::addGroup(const KeyGroup &group) { if (group.isNull()) { return QModelIndex(); } return doAddGroup(group); } bool AbstractKeyListModel::removeGroup(const KeyGroup &group) { if (group.isNull()) { return false; } return doRemoveGroup(group); } void AbstractKeyListModel::clear(ItemTypes types) { beginResetModel(); doClear(types); if (types & Keys) { d->prettyEMailCache.clear(); d->remarksCache.clear(); } endResetModel(); } int AbstractKeyListModel::columnCount(const QModelIndex &) const { return NumColumns; } QVariant AbstractKeyListModel::headerData(int section, Qt::Orientation o, int role) const { if (o == Qt::Horizontal) if (role == Qt::DisplayRole || role == Qt::EditRole || role == Qt::ToolTipRole) switch (section) { case PrettyName: return i18n("Name"); case PrettyEMail: return i18n("E-Mail"); case Validity: return i18n("User-IDs"); case ValidFrom: return i18n("Valid From"); case ValidUntil: return i18n("Valid Until"); case TechnicalDetails: return i18n("Protocol"); case ShortKeyID: return i18n("Key-ID"); case KeyID: return i18n("Key-ID"); case Fingerprint: return i18n("Fingerprint"); case Issuer: return i18n("Issuer"); case SerialNumber: return i18n("Serial Number"); case Origin: return i18n("Origin"); case LastUpdate: return i18n("Last Update"); case OwnerTrust: return i18n("Certification Trust"); case Remarks: return i18n("Tags"); case NumColumns:; } return QVariant(); } static QVariant returnIfValid(const QColor &t) { if (t.isValid()) { return t; } else { return QVariant(); } } static QVariant returnIfValid(const QIcon &t) { if (!t.isNull()) { return t; } else { return QVariant(); } } QVariant AbstractKeyListModel::data(const QModelIndex &index, int role) const { const Key key = this->key(index); if (!key.isNull()) { return data(key, index.column(), role); } const KeyGroup group = this->group(index); if (!group.isNull()) { return data(group, index.column(), role); } return QVariant(); } QVariant AbstractKeyListModel::data(const Key &key, int column, int role) const { if (role == Qt::DisplayRole || role == Qt::EditRole) { switch (column) { case PrettyName: return Formatting::prettyName(key); case PrettyEMail: if (const char *const fpr = key.primaryFingerprint()) { const QHash::const_iterator it = d->prettyEMailCache.constFind(fpr); if (it != d->prettyEMailCache.constEnd()) { return *it; } else { return d->prettyEMailCache[fpr] = Formatting::prettyEMail(key); } } else { return QVariant(); } case Validity: return Formatting::complianceStringShort(key); case ValidFrom: if (role == Qt::EditRole) { return Formatting::creationDate(key); } else { return Formatting::creationDateString(key); } case ValidUntil: if (role == Qt::EditRole) { return Formatting::expirationDate(key); } else { return Formatting::expirationDateString(key); } case TechnicalDetails: return Formatting::type(key); case ShortKeyID: return QString::fromLatin1(key.shortKeyID()); case KeyID: return Formatting::prettyID(key.keyID()); case Summary: return Formatting::summaryLine(key); case Fingerprint: return Formatting::prettyID(key.primaryFingerprint()); case Issuer: return QString::fromUtf8(key.issuerName()); case Origin: return Formatting::origin(key.origin()); case LastUpdate: return Formatting::dateString(key.lastUpdate()); case SerialNumber: return QString::fromUtf8(key.issuerSerial()); case OwnerTrust: return Formatting::ownerTrustShort(key.ownerTrust()); case Remarks: #ifdef GPGME_HAS_REMARKS { const char *const fpr = key.primaryFingerprint(); if (fpr && key.protocol() == GpgME::OpenPGP && key.numUserIDs() && d->m_remarkKeys.size()) { if (!(key.keyListMode() & GpgME::SignatureNotations)) { return i18n("Loading..."); } const QHash::const_iterator it = d->remarksCache.constFind(fpr); if (it != d->remarksCache.constEnd()) { return *it; } else { GpgME::Error err; const auto remarks = key.userID(0).remarks(d->m_remarkKeys, err); if (remarks.size() == 1) { const auto remark = QString::fromStdString(remarks[0]); return d->remarksCache[fpr] = remark; } else { QStringList remarkList; remarkList.reserve(remarks.size()); for (const auto &rem: remarks) { remarkList << QString::fromStdString(rem); } const auto remark = remarkList.join(QStringLiteral("; ")); return d->remarksCache[fpr] = remark; } } } else { return QVariant(); } } #endif return QVariant(); case NumColumns: break; } } else if (role == Qt::ToolTipRole) { return Formatting::toolTip(key, toolTipOptions()); } else if (role == Qt::FontRole) { return KeyFilterManager::instance()->font(key, (column == ShortKeyID || column == KeyID || column == Fingerprint) ? QFont(QStringLiteral("monospace")) : QFont()); } else if (role == Qt::DecorationRole) { return column == Icon ? returnIfValid(KeyFilterManager::instance()->icon(key)) : QVariant(); } else if (role == Qt::BackgroundRole) { return returnIfValid(KeyFilterManager::instance()->bgColor(key)); } else if (role == Qt::ForegroundRole) { return returnIfValid(KeyFilterManager::instance()->fgColor(key)); } else if (role == FingerprintRole) { return QString::fromLatin1(key.primaryFingerprint()); } else if (role == KeyRole) { return QVariant::fromValue(key); } return QVariant(); } QVariant AbstractKeyListModel::data(const KeyGroup &group, int column, int role) const { if (role == Qt::DisplayRole || role == Qt::EditRole) { switch (column) { case PrettyName: return group.name(); case PrettyEMail: return QVariant(); case Validity: return Formatting::complianceStringShort(group); case ValidFrom: return QString(); case ValidUntil: return QString(); case TechnicalDetails: return Formatting::type(group); case ShortKeyID: return QString(); case KeyID: return QString(); case Summary: return Formatting::summaryLine(group); // used for filtering case Fingerprint: return QString(); case Issuer: return QString(); case Origin: return QString(); case LastUpdate: return QString(); case SerialNumber: return QString(); case OwnerTrust: return QString(); case Remarks: return QVariant(); case NumColumns: break; } } else if (role == Qt::ToolTipRole) { return Formatting::toolTip(group, toolTipOptions()); } else if (role == Qt::FontRole) { return QFont(); } else if (role == Qt::DecorationRole) { return column == Icon ? QIcon::fromTheme(QStringLiteral("group")) : QVariant(); } else if (role == Qt::BackgroundRole) { } else if (role == Qt::ForegroundRole) { } else if (role == GroupRole) { return QVariant::fromValue(group); } return QVariant(); } bool AbstractKeyListModel::setData(const QModelIndex &index, const QVariant &value, int role) { Q_UNUSED(role) Q_ASSERT(value.canConvert()); if (value.canConvert()) { const KeyGroup group = value.value(); return doSetGroupData(index, group); } return false; } namespace { template class TableModelMixin : public Base { public: explicit TableModelMixin(QObject *p = nullptr) : Base(p) {} ~TableModelMixin() {} using Base::index; QModelIndex index(int row, int column, const QModelIndex &pidx = QModelIndex()) const override { return this->hasIndex(row, column, pidx) ? this->createIndex(row, column, nullptr) : QModelIndex(); } private: QModelIndex parent(const QModelIndex &) const override { return QModelIndex(); } bool hasChildren(const QModelIndex &pidx) const override { return (pidx.model() == this || !pidx.isValid()) && this->rowCount(pidx) > 0 && this->columnCount(pidx) > 0; } }; class FlatKeyListModel #ifndef Q_MOC_RUN : public TableModelMixin #else : public AbstractKeyListModel #endif { Q_OBJECT public: explicit FlatKeyListModel(QObject *parent = nullptr); ~FlatKeyListModel() override; int rowCount(const QModelIndex &pidx) const override { return pidx.isValid() ? 0 : mKeysByFingerprint.size() + mGroups.size(); } private: Key doMapToKey(const QModelIndex &index) const override; QModelIndex doMapFromKey(const Key &key, int col) const override; QList doAddKeys(const std::vector &keys) override; void doRemoveKey(const Key &key) override; KeyGroup doMapToGroup(const QModelIndex &index) const override; QModelIndex doMapFromGroup(const KeyGroup &group, int column) const override; void doSetGroups(const std::vector &groups) override; QModelIndex doAddGroup(const KeyGroup &group) override; bool doSetGroupData(const QModelIndex &index, const KeyGroup &group) override; bool doRemoveGroup(const KeyGroup &group) override; void doClear(ItemTypes types) override { if (types & Keys) { mKeysByFingerprint.clear(); } if (types & Groups) { mGroups.clear(); } } int firstGroupRow() const { return mKeysByFingerprint.size(); } int lastGroupRow() const { return mKeysByFingerprint.size() + mGroups.size() - 1; } int groupIndex(const QModelIndex &index) const { if (!index.isValid() || index.row() < firstGroupRow() || index.row() > lastGroupRow() || index.column() >= NumColumns) { return -1; } return index.row() - firstGroupRow(); } private: std::vector mKeysByFingerprint; std::vector mGroups; }; class HierarchicalKeyListModel : public AbstractKeyListModel { Q_OBJECT public: explicit HierarchicalKeyListModel(QObject *parent = nullptr); ~HierarchicalKeyListModel() override; int rowCount(const QModelIndex &pidx) const override; using AbstractKeyListModel::index; QModelIndex index(int row, int col, const QModelIndex &pidx) const override; QModelIndex parent(const QModelIndex &idx) const override; bool hasChildren(const QModelIndex &pidx) const override { return rowCount(pidx) > 0; } private: Key doMapToKey(const QModelIndex &index) const override; QModelIndex doMapFromKey(const Key &key, int col) const override; QList doAddKeys(const std::vector &keys) override; void doRemoveKey(const Key &key) override; KeyGroup doMapToGroup(const QModelIndex &index) const override; QModelIndex doMapFromGroup(const KeyGroup &group, int column) const override; void doSetGroups(const std::vector &groups) override; QModelIndex doAddGroup(const KeyGroup &group) override; bool doSetGroupData(const QModelIndex &index, const KeyGroup &group) override; bool doRemoveGroup(const KeyGroup &group) override; void doClear(ItemTypes types) override { if (types & Keys) { mTopLevels.clear(); mKeysByFingerprint.clear(); mKeysByExistingParent.clear(); mKeysByNonExistingParent.clear(); } if (types & Groups) { mGroups.clear(); } } int firstGroupRow() const { return mTopLevels.size(); } int lastGroupRow() const { return mTopLevels.size() + mGroups.size() - 1; } int groupIndex(const QModelIndex &index) const { if (!index.isValid() || index.row() < firstGroupRow() || index.row() > lastGroupRow() || index.column() >= NumColumns) { return -1; } return index.row() - firstGroupRow(); } private: void addTopLevelKey(const Key &key); void addKeyWithParent(const char *issuer_fpr, const Key &key); void addKeyWithoutParent(const char *issuer_fpr, const Key &key); private: typedef std::map< std::string, std::vector > Map; std::vector mKeysByFingerprint; // all keys Map mKeysByExistingParent, mKeysByNonExistingParent; // parent->child map std::vector mTopLevels; // all roots + parent-less std::vector mGroups; }; static const char *cleanChainID(const Key &key) { if (key.isRoot()) { return ""; } if (const char *chid = key.chainID()) { return chid; } return ""; } } FlatKeyListModel::FlatKeyListModel(QObject *p) : TableModelMixin(p) { } FlatKeyListModel::~FlatKeyListModel() {} Key FlatKeyListModel::doMapToKey(const QModelIndex &idx) const { Q_ASSERT(idx.isValid()); if (static_cast(idx.row()) < mKeysByFingerprint.size() && idx.column() < NumColumns) { return mKeysByFingerprint[ idx.row() ]; } else { return Key::null; } } QModelIndex FlatKeyListModel::doMapFromKey(const Key &key, int col) const { Q_ASSERT(!key.isNull()); const std::vector::const_iterator it = std::lower_bound(mKeysByFingerprint.begin(), mKeysByFingerprint.end(), key, _detail::ByFingerprint()); if (it == mKeysByFingerprint.end() || !_detail::ByFingerprint()(*it, key)) { return {}; } else { return createIndex(it - mKeysByFingerprint.begin(), col); } } QList FlatKeyListModel::doAddKeys(const std::vector &keys) { Q_ASSERT(std::is_sorted(keys.begin(), keys.end(), _detail::ByFingerprint())); if (keys.empty()) { return QList(); } for (auto it = keys.begin(), end = keys.end(); it != end; ++it) { // find an insertion point: const std::vector::iterator pos = std::upper_bound(mKeysByFingerprint.begin(), mKeysByFingerprint.end(), *it, _detail::ByFingerprint()); const unsigned int idx = std::distance(mKeysByFingerprint.begin(), pos); if (idx > 0 && qstrcmp(mKeysByFingerprint[idx - 1].primaryFingerprint(), it->primaryFingerprint()) == 0) { // key existed before - replace with new one: mKeysByFingerprint[idx - 1] = *it; Q_EMIT dataChanged(createIndex(idx - 1, 0), createIndex(idx - 1, NumColumns - 1)); } else { // new key - insert: beginInsertRows(QModelIndex(), idx, idx); mKeysByFingerprint.insert(pos, *it); endInsertRows(); } } return indexes(keys); } void FlatKeyListModel::doRemoveKey(const Key &key) { const std::vector::iterator it = Kleo::binary_find(mKeysByFingerprint.begin(), mKeysByFingerprint.end(), key, _detail::ByFingerprint()); if (it == mKeysByFingerprint.end()) { return; } const unsigned int row = std::distance(mKeysByFingerprint.begin(), it); beginRemoveRows(QModelIndex(), row, row); mKeysByFingerprint.erase(it); endRemoveRows(); } KeyGroup FlatKeyListModel::doMapToGroup(const QModelIndex &idx) const { Q_ASSERT(idx.isValid()); if (static_cast(idx.row()) >= mKeysByFingerprint.size() && static_cast(idx.row()) < mKeysByFingerprint.size() + mGroups.size() && idx.column() < NumColumns) { return mGroups[ idx.row() - mKeysByFingerprint.size() ]; } else { return KeyGroup(); } } QModelIndex FlatKeyListModel::doMapFromGroup(const KeyGroup &group, int column) const { Q_ASSERT(!group.isNull()); const auto it = std::find_if(mGroups.cbegin(), mGroups.cend(), [group](const KeyGroup &g) { return g.source() == group.source() && g.id() == group.id(); }); if (it == mGroups.cend()) { return QModelIndex(); } else { return createIndex(it - mGroups.cbegin() + mKeysByFingerprint.size(), column); } } void FlatKeyListModel::doSetGroups(const std::vector &groups) { Q_ASSERT(mGroups.empty()); // ensure that groups have been cleared const int first = mKeysByFingerprint.size(); const int last = first + groups.size() - 1; beginInsertRows(QModelIndex(), first, last); mGroups = groups; endInsertRows(); } QModelIndex FlatKeyListModel::doAddGroup(const KeyGroup &group) { const int newRow = lastGroupRow() + 1; beginInsertRows(QModelIndex(), newRow, newRow); mGroups.push_back(group); endInsertRows(); return createIndex(newRow, 0); } bool FlatKeyListModel::doSetGroupData(const QModelIndex &index, const KeyGroup &group) { if (group.isNull()) { return false; } const int groupIndex = this->groupIndex(index); if (groupIndex == -1) { return false; } mGroups[groupIndex] = group; Q_EMIT dataChanged(createIndex(index.row(), 0), createIndex(index.row(), NumColumns - 1)); return true; } bool FlatKeyListModel::doRemoveGroup(const KeyGroup &group) { const QModelIndex modelIndex = doMapFromGroup(group, 0); if (!modelIndex.isValid()) { return false; } const int groupIndex = this->groupIndex(modelIndex); Q_ASSERT(groupIndex != -1); if (groupIndex == -1) { return false; } beginRemoveRows(QModelIndex(), modelIndex.row(), modelIndex.row()); mGroups.erase(mGroups.begin() + groupIndex); endRemoveRows(); return true; } HierarchicalKeyListModel::HierarchicalKeyListModel(QObject *p) : AbstractKeyListModel(p), mKeysByFingerprint(), mKeysByExistingParent(), mKeysByNonExistingParent(), mTopLevels() { } HierarchicalKeyListModel::~HierarchicalKeyListModel() {} int HierarchicalKeyListModel::rowCount(const QModelIndex &pidx) const { // toplevel item: if (!pidx.isValid()) { return mTopLevels.size() + mGroups.size(); } if (pidx.column() != 0) { return 0; } // non-toplevel item - find the number of subjects for this issuer: const Key issuer = this->key(pidx); const char *const fpr = issuer.primaryFingerprint(); if (!fpr || !*fpr) { return 0; } const Map::const_iterator it = mKeysByExistingParent.find(fpr); if (it == mKeysByExistingParent.end()) { return 0; } return it->second.size(); } QModelIndex HierarchicalKeyListModel::index(int row, int col, const QModelIndex &pidx) const { if (row < 0 || col < 0 || col >= NumColumns) { return {}; } // toplevel item: if (!pidx.isValid()) { if (static_cast(row) < mTopLevels.size()) { return index(mTopLevels[row], col); } else if (static_cast(row) < mTopLevels.size() + mGroups.size()) { return index(mGroups[row - mTopLevels.size()], col); } else { return QModelIndex(); } } // non-toplevel item - find the row'th subject of this key: const Key issuer = this->key(pidx); const char *const fpr = issuer.primaryFingerprint(); if (!fpr || !*fpr) { return QModelIndex(); } const Map::const_iterator it = mKeysByExistingParent.find(fpr); if (it == mKeysByExistingParent.end() || static_cast(row) >= it->second.size()) { return QModelIndex(); } return index(it->second[row], col); } QModelIndex HierarchicalKeyListModel::parent(const QModelIndex &idx) const { const Key key = this->key(idx); if (key.isNull() || key.isRoot()) { return {}; } const std::vector::const_iterator it = Kleo::binary_find(mKeysByFingerprint.begin(), mKeysByFingerprint.end(), cleanChainID(key), _detail::ByFingerprint()); return it != mKeysByFingerprint.end() ? index(*it) : QModelIndex(); } Key HierarchicalKeyListModel::doMapToKey(const QModelIndex &idx) const { if (!idx.isValid()) { return Key::null; } const char *const issuer_fpr = static_cast(idx.internalPointer()); if (!issuer_fpr || !*issuer_fpr) { // top-level: if (static_cast(idx.row()) >= mTopLevels.size()) { return Key::null; } else { return mTopLevels[idx.row()]; } } // non-toplevel: const Map::const_iterator it = mKeysByExistingParent.find(issuer_fpr); if (it == mKeysByExistingParent.end() || static_cast(idx.row()) >= it->second.size()) { return Key::null; } return it->second[idx.row()]; } QModelIndex HierarchicalKeyListModel::doMapFromKey(const Key &key, int col) const { if (key.isNull()) { return {}; } const char *issuer_fpr = cleanChainID(key); // we need to look in the toplevels list,... const std::vector *v = &mTopLevels; if (issuer_fpr && *issuer_fpr) { const std::map< std::string, std::vector >::const_iterator it = mKeysByExistingParent.find(issuer_fpr); // ...unless we find an existing parent: if (it != mKeysByExistingParent.end()) { v = &it->second; } else { issuer_fpr = nullptr; // force internalPointer to zero for toplevels } } const std::vector::const_iterator it = std::lower_bound(v->begin(), v->end(), key, _detail::ByFingerprint()); if (it == v->end() || !_detail::ByFingerprint()(*it, key)) { return QModelIndex(); } const unsigned int row = std::distance(v->begin(), it); return createIndex(row, col, const_cast(issuer_fpr)); } void HierarchicalKeyListModel::addKeyWithParent(const char *issuer_fpr, const Key &key) { Q_ASSERT(issuer_fpr); Q_ASSERT(*issuer_fpr); Q_ASSERT(!key.isNull()); std::vector &subjects = mKeysByExistingParent[issuer_fpr]; // find insertion point: const std::vector::iterator it = std::lower_bound(subjects.begin(), subjects.end(), key, _detail::ByFingerprint()); const int row = std::distance(subjects.begin(), it); if (it != subjects.end() && qstricmp(it->primaryFingerprint(), key.primaryFingerprint()) == 0) { // exists -> replace *it = key; Q_EMIT dataChanged(createIndex(row, 0, const_cast(issuer_fpr)), createIndex(row, NumColumns - 1, const_cast(issuer_fpr))); } else { // doesn't exist -> insert const std::vector::const_iterator pos = Kleo::binary_find(mKeysByFingerprint.begin(), mKeysByFingerprint.end(), issuer_fpr, _detail::ByFingerprint()); Q_ASSERT(pos != mKeysByFingerprint.end()); beginInsertRows(index(*pos), row, row); subjects.insert(it, key); endInsertRows(); } } void HierarchicalKeyListModel::addKeyWithoutParent(const char *issuer_fpr, const Key &key) { Q_ASSERT(issuer_fpr); Q_ASSERT(*issuer_fpr); Q_ASSERT(!key.isNull()); std::vector &subjects = mKeysByNonExistingParent[issuer_fpr]; // find insertion point: const std::vector::iterator it = std::lower_bound(subjects.begin(), subjects.end(), key, _detail::ByFingerprint()); if (it != subjects.end() && qstricmp(it->primaryFingerprint(), key.primaryFingerprint()) == 0) // exists -> replace { *it = key; } else // doesn't exist -> insert { subjects.insert(it, key); } addTopLevelKey(key); } void HierarchicalKeyListModel::addTopLevelKey(const Key &key) { // find insertion point: const std::vector::iterator it = std::lower_bound(mTopLevels.begin(), mTopLevels.end(), key, _detail::ByFingerprint()); const int row = std::distance(mTopLevels.begin(), it); if (it != mTopLevels.end() && qstricmp(it->primaryFingerprint(), key.primaryFingerprint()) == 0) { // exists -> replace *it = key; Q_EMIT dataChanged(createIndex(row, 0), createIndex(row, NumColumns - 1)); } else { // doesn't exist -> insert beginInsertRows(QModelIndex(), row, row); mTopLevels.insert(it, key); endInsertRows(); } } // sorts 'keys' such that parent always come before their children: static std::vector topological_sort(const std::vector &keys) { boost::adjacency_list<> graph(keys.size()); // add edges from children to parents: for (unsigned int i = 0, end = keys.size(); i != end; ++i) { const char *const issuer_fpr = cleanChainID(keys[i]); if (!issuer_fpr || !*issuer_fpr) { continue; } const std::vector::const_iterator it = Kleo::binary_find(keys.begin(), keys.end(), issuer_fpr, _detail::ByFingerprint()); if (it == keys.end()) { continue; } add_edge(i, std::distance(keys.begin(), it), graph); } std::vector order; order.reserve(keys.size()); topological_sort(graph, std::back_inserter(order)); Q_ASSERT(order.size() == keys.size()); std::vector result; result.reserve(keys.size()); for (int i : qAsConst(order)) { result.push_back(keys[i]); } return result; } QList HierarchicalKeyListModel::doAddKeys(const std::vector &keys) { Q_ASSERT(std::is_sorted(keys.begin(), keys.end(), _detail::ByFingerprint())); if (keys.empty()) { return QList(); } const std::vector oldKeys = mKeysByFingerprint; std::vector merged; merged.reserve(keys.size() + mKeysByFingerprint.size()); std::set_union(keys.begin(), keys.end(), mKeysByFingerprint.begin(), mKeysByFingerprint.end(), std::back_inserter(merged), _detail::ByFingerprint()); mKeysByFingerprint = merged; std::set > changedParents; const auto topologicalSortedList = topological_sort(keys); for (const Key &key : topologicalSortedList) { // check to see whether this key is a parent for a previously parent-less group: const char *const fpr = key.primaryFingerprint(); if (!fpr || !*fpr) { continue; } const bool keyAlreadyExisted = std::binary_search(oldKeys.begin(), oldKeys.end(), key, _detail::ByFingerprint()); const Map::iterator it = mKeysByNonExistingParent.find(fpr); const std::vector children = it != mKeysByNonExistingParent.end() ? it->second : std::vector(); if (it != mKeysByNonExistingParent.end()) { mKeysByNonExistingParent.erase(it); } // Step 1: For new keys, remove children from toplevel: if (!keyAlreadyExisted) { auto last = mTopLevels.begin(); auto lastFP = mKeysByFingerprint.begin(); for (const Key &k : qAsConst(children)) { last = Kleo::binary_find(last, mTopLevels.end(), k, _detail::ByFingerprint()); Q_ASSERT(last != mTopLevels.end()); const int row = std::distance(mTopLevels.begin(), last); lastFP = Kleo::binary_find(lastFP, mKeysByFingerprint.end(), k, _detail::ByFingerprint()); Q_ASSERT(lastFP != mKeysByFingerprint.end()); Q_EMIT rowAboutToBeMoved(QModelIndex(), row); beginRemoveRows(QModelIndex(), row, row); last = mTopLevels.erase(last); lastFP = mKeysByFingerprint.erase(lastFP); endRemoveRows(); } } // Step 2: add/update key const char *const issuer_fpr = cleanChainID(key); if (!issuer_fpr || !*issuer_fpr) // root or something... { addTopLevelKey(key); } else if (std::binary_search(mKeysByFingerprint.begin(), mKeysByFingerprint.end(), issuer_fpr, _detail::ByFingerprint())) // parent exists... { addKeyWithParent(issuer_fpr, key); } else // parent doesn't exist yet... { addKeyWithoutParent(issuer_fpr, key); } const QModelIndex key_idx = index(key); QModelIndex key_parent = key_idx.parent(); while (key_parent.isValid()) { changedParents.insert(doMapToKey(key_parent)); key_parent = key_parent.parent(); } // Step 3: Add children to new parent ( == key ) if (!keyAlreadyExisted && !children.empty()) { addKeys(children); const QModelIndex new_parent = index(key); // Q_EMIT the rowMoved() signals in reversed direction, so the // implementation can use a stack for mapping. for (int i = children.size() - 1; i >= 0; --i) { Q_EMIT rowMoved(new_parent, i); } } } //Q_EMIT dataChanged for all parents with new children. This triggers KeyListSortFilterProxyModel to //show a parent node if it just got children matching the proxy's filter for (const Key &i : qAsConst(changedParents)) { const QModelIndex idx = index(i); if (idx.isValid()) { Q_EMIT dataChanged(idx.sibling(idx.row(), 0), idx.sibling(idx.row(), NumColumns - 1)); } } return indexes(keys); } void HierarchicalKeyListModel::doRemoveKey(const Key &key) { const QModelIndex idx = index(key); if (!idx.isValid()) { return; } const char *const fpr = key.primaryFingerprint(); if (mKeysByExistingParent.find(fpr) != mKeysByExistingParent.end()) { //handle non-leave nodes: std::vector keys = mKeysByFingerprint; const std::vector::iterator it = Kleo::binary_find(keys.begin(), keys.end(), key, _detail::ByFingerprint()); if (it == keys.end()) { return; } keys.erase(it); // FIXME for simplicity, we just clear the model and re-add all keys minus the removed one. This is suboptimal, // but acceptable given that deletion of non-leave nodes is rather rare. clear(Keys); addKeys(keys); return; } //handle leave nodes: const std::vector::iterator it = Kleo::binary_find(mKeysByFingerprint.begin(), mKeysByFingerprint.end(), key, _detail::ByFingerprint()); Q_ASSERT(it != mKeysByFingerprint.end()); Q_ASSERT(mKeysByNonExistingParent.find(fpr) == mKeysByNonExistingParent.end()); Q_ASSERT(mKeysByExistingParent.find(fpr) == mKeysByExistingParent.end()); beginRemoveRows(parent(idx), idx.row(), idx.row()); mKeysByFingerprint.erase(it); const char *const issuer_fpr = cleanChainID(key); const std::vector::iterator tlIt = Kleo::binary_find(mTopLevels.begin(), mTopLevels.end(), key, _detail::ByFingerprint()); if (tlIt != mTopLevels.end()) { mTopLevels.erase(tlIt); } if (issuer_fpr && *issuer_fpr) { const Map::iterator nexIt = mKeysByNonExistingParent.find(issuer_fpr); if (nexIt != mKeysByNonExistingParent.end()) { const std::vector::iterator eit = Kleo::binary_find(nexIt->second.begin(), nexIt->second.end(), key, _detail::ByFingerprint()); if (eit != nexIt->second.end()) { nexIt->second.erase(eit); } if (nexIt->second.empty()) { mKeysByNonExistingParent.erase(nexIt); } } const Map::iterator exIt = mKeysByExistingParent.find(issuer_fpr); if (exIt != mKeysByExistingParent.end()) { const std::vector::iterator eit = Kleo::binary_find(exIt->second.begin(), exIt->second.end(), key, _detail::ByFingerprint()); if (eit != exIt->second.end()) { exIt->second.erase(eit); } if (exIt->second.empty()) { mKeysByExistingParent.erase(exIt); } } } endRemoveRows(); } KeyGroup HierarchicalKeyListModel::doMapToGroup(const QModelIndex &idx) const { Q_ASSERT(idx.isValid()); if (idx.parent().isValid()) { // groups are always top-level return KeyGroup(); } if (static_cast(idx.row()) >= mTopLevels.size() && static_cast(idx.row()) < mTopLevels.size() + mGroups.size() && idx.column() < NumColumns) { return mGroups[ idx.row() - mTopLevels.size() ]; } else { return KeyGroup(); } } QModelIndex HierarchicalKeyListModel::doMapFromGroup(const KeyGroup &group, int column) const { Q_ASSERT(!group.isNull()); const auto it = std::find_if(mGroups.cbegin(), mGroups.cend(), [group](const KeyGroup &g) { return g.source() == group.source() && g.id() == group.id(); }); if (it == mGroups.cend()) { return QModelIndex(); } else { return createIndex(it - mGroups.cbegin() + mTopLevels.size(), column); } } void HierarchicalKeyListModel::doSetGroups(const std::vector &groups) { Q_ASSERT(mGroups.empty()); // ensure that groups have been cleared const int first = mTopLevels.size(); const int last = first + groups.size() - 1; beginInsertRows(QModelIndex(), first, last); mGroups = groups; endInsertRows(); } QModelIndex HierarchicalKeyListModel::doAddGroup(const KeyGroup &group) { const int newRow = lastGroupRow() + 1; beginInsertRows(QModelIndex(), newRow, newRow); mGroups.push_back(group); endInsertRows(); return createIndex(newRow, 0); } bool HierarchicalKeyListModel::doSetGroupData(const QModelIndex &index, const KeyGroup &group) { if (group.isNull()) { return false; } const int groupIndex = this->groupIndex(index); if (groupIndex == -1) { return false; } mGroups[groupIndex] = group; Q_EMIT dataChanged(createIndex(index.row(), 0), createIndex(index.row(), NumColumns - 1)); return true; } bool HierarchicalKeyListModel::doRemoveGroup(const KeyGroup &group) { const QModelIndex modelIndex = doMapFromGroup(group, 0); if (!modelIndex.isValid()) { return false; } const int groupIndex = this->groupIndex(modelIndex); Q_ASSERT(groupIndex != -1); if (groupIndex == -1) { return false; } beginRemoveRows(QModelIndex(), modelIndex.row(), modelIndex.row()); mGroups.erase(mGroups.begin() + groupIndex); endRemoveRows(); return true; } void AbstractKeyListModel::useKeyCache(bool value, KeyList::Options options) { d->m_keyListOptions = options; d->m_useKeyCache = value; if (!d->m_useKeyCache) { clear(All); } else { d->updateFromKeyCache(); } connect(KeyCache::instance().get(), &KeyCache::keysMayHaveChanged, this, [this] { d->updateFromKeyCache(); }); } // static AbstractKeyListModel *AbstractKeyListModel::createFlatKeyListModel(QObject *p) { AbstractKeyListModel *const m = new FlatKeyListModel(p); #ifdef KLEO_MODEL_TEST new QAbstractItemModelTester(m, p); #endif return m; } // static AbstractKeyListModel *AbstractKeyListModel::createHierarchicalKeyListModel(QObject *p) { AbstractKeyListModel *const m = new HierarchicalKeyListModel(p); #ifdef KLEO_MODEL_TEST new QAbstractItemModelTester(m, p); #endif return m; } #include "keylistmodel.moc" /*! \fn AbstractKeyListModel::rowAboutToBeMoved( const QModelIndex & old_parent, int old_row ) Emitted before the removal of a row from that model. It will later be added to the model again, in response to which rowMoved() will be emitted. If multiple rows are moved in one go, multiple rowAboutToBeMoved() signals are emitted before the corresponding number of rowMoved() signals is emitted - in reverse order. This works around the absence of move semantics in QAbstractItemModel. Clients can maintain a stack to perform the QModelIndex-mapping themselves, or, e.g., to preserve the selection status of the row: \code std::vector mMovingRowWasSelected; // transient, used when rows are moved // ... void slotRowAboutToBeMoved( const QModelIndex & p, int row ) { mMovingRowWasSelected.push_back( selectionModel()->isSelected( model()->index( row, 0, p ) ) ); } void slotRowMoved( const QModelIndex & p, int row ) { const bool wasSelected = mMovingRowWasSelected.back(); mMovingRowWasSelected.pop_back(); if ( wasSelected ) selectionModel()->select( model()->index( row, 0, p ), Select|Rows ); } \endcode A similar mechanism could be used to preserve the current item during moves. */ /*! \fn AbstractKeyListModel::rowMoved( const QModelIndex & new_parent, int new_parent ) See rowAboutToBeMoved() */ diff --git a/src/ui/keyrequester.cpp b/src/ui/keyrequester.cpp index 030f1249f..1dfb0e2a8 100644 --- a/src/ui/keyrequester.cpp +++ b/src/ui/keyrequester.cpp @@ -1,490 +1,489 @@ /* -*- c++ -*- keyrequester.cpp This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2004 Klarälvdalens Datakonsult AB Based on kpgpui.cpp SPDX-FileCopyrightText: 2001, 2002 the KPGP authors See file libkdenetwork/AUTHORS.kpgp for details This file is part of KPGP, the KDE PGP/GnuPG support library. SPDX-License-Identifier: GPL-2.0-or-later */ #include "keyrequester.h" #include "keyselectiondialog.h" #include "libkleo/dn.h" // gpgme++ #include #include #include // KDE #include #include #include #include // Qt #include #include -#include #include using namespace QGpgME; Kleo::KeyRequester::KeyRequester(unsigned int allowedKeys, bool multipleKeys, QWidget *parent) : QWidget(parent), mOpenPGPBackend(nullptr), mSMIMEBackend(nullptr), mMulti(multipleKeys), mKeyUsage(allowedKeys), mJobs(0), d(nullptr) { init(); } Kleo::KeyRequester::KeyRequester(QWidget *parent) : QWidget(parent), mOpenPGPBackend(nullptr), mSMIMEBackend(nullptr), mMulti(false), mKeyUsage(0), mJobs(0), d(nullptr) { init(); } void Kleo::KeyRequester::init() { auto hlay = new QHBoxLayout(this); hlay->setContentsMargins(0, 0, 0, 0); // the label where the key id is to be displayed: mLabel = new QLabel(this); mLabel->setFrameStyle(QFrame::StyledPanel | QFrame::Sunken); // the button to unset any key: mEraseButton = new QPushButton(this); mEraseButton->setAutoDefault(false); mEraseButton->setSizePolicy(QSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum)); mEraseButton->setIcon(QIcon::fromTheme(QApplication::isRightToLeft() ? QStringLiteral("edit-clear-locationbar-ltr") : QStringLiteral("edit-clear-locationbar-rtl"))); mEraseButton->setToolTip(i18n("Clear")); // the button to call the KeySelectionDialog: mDialogButton = new QPushButton(i18n("Change..."), this); mDialogButton->setAutoDefault(false); hlay->addWidget(mLabel, 1); hlay->addWidget(mEraseButton); hlay->addWidget(mDialogButton); connect(mEraseButton, &QPushButton::clicked, this, &SigningKeyRequester::slotEraseButtonClicked); connect(mDialogButton, &QPushButton::clicked, this, &SigningKeyRequester::slotDialogButtonClicked); setSizePolicy(QSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed)); setAllowedKeys(mKeyUsage); } Kleo::KeyRequester::~KeyRequester() { } const std::vector &Kleo::KeyRequester::keys() const { return mKeys; } const GpgME::Key &Kleo::KeyRequester::key() const { static const GpgME::Key null = GpgME::Key::null; if (mKeys.empty()) { return null; } else { return mKeys.front(); } } void Kleo::KeyRequester::setKeys(const std::vector &keys) { mKeys.clear(); for (auto it = keys.begin(); it != keys.end(); ++it) if (!it->isNull()) { mKeys.push_back(*it); } updateKeys(); } void Kleo::KeyRequester::setKey(const GpgME::Key &key) { mKeys.clear(); if (!key.isNull()) { mKeys.push_back(key); } updateKeys(); } QString Kleo::KeyRequester::fingerprint() const { if (mKeys.empty()) { return QString(); } else { return QLatin1String(mKeys.front().primaryFingerprint()); } } QStringList Kleo::KeyRequester::fingerprints() const { QStringList result; for (auto it = mKeys.begin(); it != mKeys.end(); ++it) if (!it->isNull()) if (const char *fpr = it->primaryFingerprint()) { result.push_back(QLatin1String(fpr)); } return result; } void Kleo::KeyRequester::setFingerprint(const QString &fingerprint) { startKeyListJob(QStringList(fingerprint)); } void Kleo::KeyRequester::setFingerprints(const QStringList &fingerprints) { startKeyListJob(fingerprints); } void Kleo::KeyRequester::updateKeys() { if (mKeys.empty()) { mLabel->clear(); return; } if (mKeys.size() > 1) { setMultipleKeysEnabled(true); } QStringList labelTexts; QString toolTipText; for (std::vector::const_iterator it = mKeys.begin(); it != mKeys.end(); ++it) { if (it->isNull()) { continue; } const QString fpr = QLatin1String(it->primaryFingerprint()); labelTexts.push_back(fpr.right(8)); toolTipText += fpr.right(8) + QLatin1String(": "); if (const char *uid = it->userID(0).id()) if (it->protocol() == GpgME::OpenPGP) { toolTipText += QString::fromUtf8(uid); } else { toolTipText += Kleo::DN(uid).prettyDN(); } else { toolTipText += xi18n("unknown"); } toolTipText += QLatin1Char('\n'); } mLabel->setText(labelTexts.join(QLatin1String(", "))); mLabel->setToolTip(toolTipText); } #ifndef __KLEO_UI_SHOW_KEY_LIST_ERROR_H__ #define __KLEO_UI_SHOW_KEY_LIST_ERROR_H__ static void showKeyListError(QWidget *parent, const GpgME::Error &err) { Q_ASSERT(err); const QString msg = i18n("

An error occurred while fetching " "the keys from the backend:

" "

%1

", QString::fromLocal8Bit(err.asString())); KMessageBox::error(parent, msg, i18n("Key Listing Failed")); } #endif // __KLEO_UI_SHOW_KEY_LIST_ERROR_H__ void Kleo::KeyRequester::startKeyListJob(const QStringList &fingerprints) { if (!mSMIMEBackend && !mOpenPGPBackend) { return; } mTmpKeys.clear(); mJobs = 0; unsigned int count = 0; for (QStringList::const_iterator it = fingerprints.begin(); it != fingerprints.end(); ++it) if (!(*it).trimmed().isEmpty()) { ++count; } if (!count) { // don't fall into the trap that an empty pattern means // "return all keys" :) setKey(GpgME::Key::null); return; } if (mOpenPGPBackend) { KeyListJob *job = mOpenPGPBackend->keyListJob(false); // local, no sigs if (!job) { KMessageBox::error(this, i18n("The OpenPGP backend does not support listing keys. " "Check your installation."), i18n("Key Listing Failed")); } else { connect(job, &KeyListJob::result, this, &SigningKeyRequester::slotKeyListResult); connect(job, &KeyListJob::nextKey, this, &SigningKeyRequester::slotNextKey); const GpgME::Error err = job->start(fingerprints, mKeyUsage & Kleo::KeySelectionDialog::SecretKeys && !(mKeyUsage & Kleo::KeySelectionDialog::PublicKeys)); if (err) { showKeyListError(this, err); } else { ++mJobs; } } } if (mSMIMEBackend) { KeyListJob *job = mSMIMEBackend->keyListJob(false); // local, no sigs if (!job) { KMessageBox::error(this, i18n("The S/MIME backend does not support listing keys. " "Check your installation."), i18n("Key Listing Failed")); } else { connect(job, &KeyListJob::result, this, &SigningKeyRequester::slotKeyListResult); connect(job, &KeyListJob::nextKey, this, &SigningKeyRequester::slotNextKey); const GpgME::Error err = job->start(fingerprints, mKeyUsage & Kleo::KeySelectionDialog::SecretKeys && !(mKeyUsage & Kleo::KeySelectionDialog::PublicKeys)); if (err) { showKeyListError(this, err); } else { ++mJobs; } } } if (mJobs > 0) { mEraseButton->setEnabled(false); mDialogButton->setEnabled(false); } } void Kleo::KeyRequester::slotNextKey(const GpgME::Key &key) { if (!key.isNull()) { mTmpKeys.push_back(key); } } void Kleo::KeyRequester::slotKeyListResult(const GpgME::KeyListResult &res) { if (res.error()) { showKeyListError(this, res.error()); } if (--mJobs <= 0) { mEraseButton->setEnabled(true); mDialogButton->setEnabled(true); setKeys(mTmpKeys); mTmpKeys.clear(); } } void Kleo::KeyRequester::slotDialogButtonClicked() { KeySelectionDialog *dlg = mKeys.empty() ? new KeySelectionDialog(mDialogCaption, mDialogMessage, mInitialQuery, mKeyUsage, mMulti, false, this) : new KeySelectionDialog(mDialogCaption, mDialogCaption, mKeys, mKeyUsage, mMulti, false, this); if (dlg->exec() == QDialog::Accepted) { if (mMulti) { setKeys(dlg->selectedKeys()); } else { setKey(dlg->selectedKey()); } Q_EMIT changed(); } delete dlg; } void Kleo::KeyRequester::slotEraseButtonClicked() { if (!mKeys.empty()) { Q_EMIT changed(); } mKeys.clear(); updateKeys(); } void Kleo::KeyRequester::setDialogCaption(const QString &caption) { mDialogCaption = caption; } void Kleo::KeyRequester::setDialogMessage(const QString &msg) { mDialogMessage = msg; } bool Kleo::KeyRequester::isMultipleKeysEnabled() const { return mMulti; } void Kleo::KeyRequester::setMultipleKeysEnabled(bool multi) { if (multi == mMulti) { return; } if (!multi && !mKeys.empty()) { mKeys.erase(mKeys.begin() + 1, mKeys.end()); } mMulti = multi; updateKeys(); } unsigned int Kleo::KeyRequester::allowedKeys() const { return mKeyUsage; } void Kleo::KeyRequester::setAllowedKeys(unsigned int keyUsage) { mKeyUsage = keyUsage; mOpenPGPBackend = nullptr; mSMIMEBackend = nullptr; if (mKeyUsage & KeySelectionDialog::OpenPGPKeys) { mOpenPGPBackend = openpgp(); } if (mKeyUsage & KeySelectionDialog::SMIMEKeys) { mSMIMEBackend = smime(); } if (mOpenPGPBackend && !mSMIMEBackend) { mDialogCaption = i18n("OpenPGP Key Selection"); mDialogMessage = i18n("Please select an OpenPGP key to use."); } else if (!mOpenPGPBackend && mSMIMEBackend) { mDialogCaption = i18n("S/MIME Key Selection"); mDialogMessage = i18n("Please select an S/MIME key to use."); } else { mDialogCaption = i18n("Key Selection"); mDialogMessage = i18n("Please select an (OpenPGP or S/MIME) key to use."); } } QPushButton *Kleo::KeyRequester::dialogButton() { return mDialogButton; } QPushButton *Kleo::KeyRequester::eraseButton() { return mEraseButton; } static inline unsigned int foo(bool openpgp, bool smime, bool trusted, bool valid) { unsigned int result = 0; if (openpgp) { result |= Kleo::KeySelectionDialog::OpenPGPKeys; } if (smime) { result |= Kleo::KeySelectionDialog::SMIMEKeys; } if (trusted) { result |= Kleo::KeySelectionDialog::TrustedKeys; } if (valid) { result |= Kleo::KeySelectionDialog::ValidKeys; } return result; } static inline unsigned int encryptionKeyUsage(bool openpgp, bool smime, bool trusted, bool valid) { return foo(openpgp, smime, trusted, valid) | Kleo::KeySelectionDialog::EncryptionKeys | Kleo::KeySelectionDialog::PublicKeys; } static inline unsigned int signingKeyUsage(bool openpgp, bool smime, bool trusted, bool valid) { return foo(openpgp, smime, trusted, valid) | Kleo::KeySelectionDialog::SigningKeys | Kleo::KeySelectionDialog::SecretKeys; } Kleo::EncryptionKeyRequester::EncryptionKeyRequester(bool multi, unsigned int proto, QWidget *parent, bool onlyTrusted, bool onlyValid) : KeyRequester(encryptionKeyUsage(proto & OpenPGP, proto & SMIME, onlyTrusted, onlyValid), multi, parent), d(nullptr) { } Kleo::EncryptionKeyRequester::EncryptionKeyRequester(QWidget *parent) : KeyRequester(0, false, parent), d(nullptr) { } Kleo::EncryptionKeyRequester::~EncryptionKeyRequester() {} void Kleo::EncryptionKeyRequester::setAllowedKeys(unsigned int proto, bool onlyTrusted, bool onlyValid) { KeyRequester::setAllowedKeys(encryptionKeyUsage(proto & OpenPGP, proto & SMIME, onlyTrusted, onlyValid)); } Kleo::SigningKeyRequester::SigningKeyRequester(bool multi, unsigned int proto, QWidget *parent, bool onlyTrusted, bool onlyValid) : KeyRequester(signingKeyUsage(proto & OpenPGP, proto & SMIME, onlyTrusted, onlyValid), multi, parent), d(nullptr) { } Kleo::SigningKeyRequester::SigningKeyRequester(QWidget *parent) : KeyRequester(0, false, parent), d(nullptr) { } Kleo::SigningKeyRequester::~SigningKeyRequester() {} void Kleo::SigningKeyRequester::setAllowedKeys(unsigned int proto, bool onlyTrusted, bool onlyValid) { KeyRequester::setAllowedKeys(signingKeyUsage(proto & OpenPGP, proto & SMIME, onlyTrusted, onlyValid)); } void Kleo::KeyRequester::virtual_hook(int, void *) {} void Kleo::EncryptionKeyRequester::virtual_hook(int id, void *data) { KeyRequester::virtual_hook(id, data); } void Kleo::SigningKeyRequester::virtual_hook(int id, void *data) { KeyRequester::virtual_hook(id, data); } diff --git a/src/utils/filesystemwatcher.cpp b/src/utils/filesystemwatcher.cpp index baf8a2df0..8cc9767ce 100644 --- a/src/utils/filesystemwatcher.cpp +++ b/src/utils/filesystemwatcher.cpp @@ -1,311 +1,310 @@ /* -*- mode: c++; c-basic-offset:4 -*- filesystemwatcher.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #include "filesystemwatcher.h" #include "kleo/stl_util.h" #include #include #include -#include #include #include #include using namespace Kleo; class FileSystemWatcher::Private { FileSystemWatcher *const q; public: explicit Private(FileSystemWatcher *qq, const QStringList &paths = QStringList()); ~Private() { delete m_watcher; } void onFileChanged(const QString &path); void onDirectoryChanged(const QString &path); void handleTimer(); void onTimeout(); void connectWatcher(); QFileSystemWatcher *m_watcher = nullptr; QTimer m_timer; std::set m_seenPaths; std::set m_cachedDirectories; std::set m_cachedFiles; QStringList m_paths, m_blacklist, m_whitelist; }; FileSystemWatcher::Private::Private(FileSystemWatcher *qq, const QStringList &paths) : q(qq), m_watcher(nullptr), m_paths(paths) { m_timer.setSingleShot(true); connect(&m_timer, &QTimer::timeout, q, [this]() { onTimeout(); }); } static bool is_matching(const QString &file, const QStringList &list) { for (const QString &entry : list) if (QRegExp(entry, Qt::CaseInsensitive, QRegExp::Wildcard).exactMatch(file)) { return true; } return false; } static bool is_blacklisted(const QString &file, const QStringList &blacklist) { return is_matching(file, blacklist); } static bool is_whitelisted(const QString &file, const QStringList &whitelist) { if (whitelist.empty()) { return true; // special case } return is_matching(file, whitelist); } void FileSystemWatcher::Private::onFileChanged(const QString &path) { const QFileInfo fi(path); if (is_blacklisted(fi.fileName(), m_blacklist)) { return; } if (!is_whitelisted(fi.fileName(), m_whitelist)) { return; } qCDebug(LIBKLEO_LOG) << path; m_seenPaths.insert(path); m_cachedFiles.insert(path); handleTimer(); } static QStringList list_dir_absolute(const QString &path, const QStringList &blacklist, const QStringList &whitelist) { QDir dir(path); QStringList entries = dir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot); QStringList::iterator end = std::remove_if(entries.begin(), entries.end(), [&blacklist](const QString &entry) { return is_blacklisted(entry, blacklist); }); if (!whitelist.empty()) end = std::remove_if(entries.begin(), end, [&whitelist](const QString &entry) { return !is_whitelisted(entry, whitelist); }); entries.erase(end, entries.end()); std::sort(entries.begin(), entries.end()); std::transform(entries.begin(), entries.end(), entries.begin(), [&dir](const QString &entry) { return dir.absoluteFilePath(entry); }); return entries; } static QStringList find_new_files(const QStringList ¤t, const std::set &seen) { QStringList result; std::set_difference(current.begin(), current.end(), seen.begin(), seen.end(), std::back_inserter(result)); return result; } void FileSystemWatcher::Private::onDirectoryChanged(const QString &path) { const QStringList newFiles = find_new_files(list_dir_absolute(path, m_blacklist, m_whitelist), m_seenPaths); if (newFiles.empty()) { return; } qCDebug(LIBKLEO_LOG) << "newFiles" << newFiles; m_cachedFiles.insert(newFiles.begin(), newFiles.end()); q->addPaths(newFiles); m_cachedDirectories.insert(path); handleTimer(); } void FileSystemWatcher::Private::onTimeout() { std::set dirs, files; dirs.swap(m_cachedDirectories); files.swap(m_cachedFiles); if (dirs.empty() && files.empty()) { return; } Q_EMIT q->triggered(); for (const QString &i : qAsConst(dirs)) { Q_EMIT q->directoryChanged(i); } for (const QString &i : qAsConst(files)) { Q_EMIT q->fileChanged(i); } } void FileSystemWatcher::Private::handleTimer() { if (m_timer.interval() == 0) { onTimeout(); return; } m_timer.start(); } void FileSystemWatcher::Private::connectWatcher() { if (!m_watcher) { return; } connect(m_watcher, &QFileSystemWatcher::directoryChanged, q, [this](const QString &str) { onDirectoryChanged(str); }); connect(m_watcher, &QFileSystemWatcher::fileChanged, q, [this](const QString &str) { onFileChanged(str); }); } FileSystemWatcher::FileSystemWatcher(QObject *p) : QObject(p), d(new Private(this)) { setEnabled(true); } FileSystemWatcher::FileSystemWatcher(const QStringList &paths, QObject *p) : QObject(p), d(new Private(this, paths)) { setEnabled(true); } void FileSystemWatcher::setEnabled(bool enable) { if (isEnabled() == enable) { return; } if (enable) { Q_ASSERT(!d->m_watcher); d->m_watcher = new QFileSystemWatcher; if (!d->m_paths.empty()) { d->m_watcher->addPaths(d->m_paths); } d->connectWatcher(); } else { Q_ASSERT(d->m_watcher); delete d->m_watcher; d->m_watcher = nullptr; } } bool FileSystemWatcher::isEnabled() const { return d->m_watcher != nullptr; } FileSystemWatcher::~FileSystemWatcher() { } void FileSystemWatcher::setDelay(int ms) { Q_ASSERT(ms >= 0); d->m_timer.setInterval(ms); } int FileSystemWatcher::delay() const { return d->m_timer.interval(); } void FileSystemWatcher::blacklistFiles(const QStringList &paths) { d->m_blacklist += paths; QStringList blacklisted; d->m_paths.erase(kdtools::separate_if(d->m_paths.begin(), d->m_paths.end(), std::back_inserter(blacklisted), d->m_paths.begin(), [this](const QString &path) { return is_blacklisted(path, d->m_blacklist); }).second, d->m_paths.end()); if (d->m_watcher && !blacklisted.empty()) { d->m_watcher->removePaths(blacklisted); } } void FileSystemWatcher::whitelistFiles(const QStringList &patterns) { d->m_whitelist += patterns; // ### would be nice to add newly-matching paths here right away, // ### but it's not as simple as blacklisting above, esp. since we // ### don't want to subject addPath()'ed paths to whitelisting. } static QStringList resolve(const QStringList &paths, const QStringList &blacklist, const QStringList &whitelist) { if (paths.empty()) { return QStringList(); } QStringList result; for (const QString &path : paths) if (QDir(path).exists()) { result += list_dir_absolute(path, blacklist, whitelist); } return result + resolve(result, blacklist, whitelist); } void FileSystemWatcher::addPaths(const QStringList &paths) { if (paths.empty()) { return; } const QStringList newPaths = paths + resolve(paths, d->m_blacklist, d->m_whitelist); if (!newPaths.empty()) { qCDebug(LIBKLEO_LOG) << "adding\n " << newPaths.join(QLatin1String("\n ")) << "\n/end"; } d->m_paths += newPaths; d->m_seenPaths.insert(newPaths.begin(), newPaths.end()); if (d->m_watcher && !newPaths.empty()) { d->m_watcher->addPaths(newPaths); } } void FileSystemWatcher::addPath(const QString &path) { addPaths(QStringList(path)); } void FileSystemWatcher::removePaths(const QStringList &paths) { if (paths.empty()) { return; } for (const QString &i : paths) { d->m_paths.removeAll(i); } if (d->m_watcher) { d->m_watcher->removePaths(paths); } } void FileSystemWatcher::removePath(const QString &path) { removePaths(QStringList(path)); } #include "moc_filesystemwatcher.cpp" diff --git a/src/utils/formatting.cpp b/src/utils/formatting.cpp index 8b18b586a..56e20564c 100644 --- a/src/utils/formatting.cpp +++ b/src/utils/formatting.cpp @@ -1,1252 +1,1251 @@ /* -*- mode: c++; c-basic-offset: 4; indent-tabs-mode: nil; -*- utils/formatting.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "formatting.h" #include "kleo/dn.h" #include "kleo/keyfiltermanager.h" #include "kleo/keygroup.h" #include "utils/cryptoconfig.h" #include #include #include #include #include #include #include -#include #include #include // for Qt::escape #include #include #include #include "models/keycache.h" using namespace GpgME; using namespace Kleo; // // Name // QString Formatting::prettyName(int proto, const char *id, const char *name_, const char *comment_) { if (proto == OpenPGP) { const QString name = QString::fromUtf8(name_); if (name.isEmpty()) { return QString(); } const QString comment = QString::fromUtf8(comment_); if (comment.isEmpty()) { return name; } return QStringLiteral("%1 (%2)").arg(name, comment); } if (proto == CMS) { const DN subject(id); const QString cn = subject[QStringLiteral("CN")].trimmed(); if (cn.isEmpty()) { return subject.prettyDN(); } return cn; } return QString(); } QString Formatting::prettyNameAndEMail(int proto, const char *id, const char *name_, const char *email_, const char *comment_) { return prettyNameAndEMail(proto, QString::fromUtf8(id), QString::fromUtf8(name_), prettyEMail(email_, id), QString::fromUtf8(comment_)); } QString Formatting::prettyNameAndEMail(int proto, const QString &id, const QString &name, const QString &email, const QString &comment) { if (proto == OpenPGP) { if (name.isEmpty()) { if (email.isEmpty()) { return QString(); } else if (comment.isEmpty()) { return QStringLiteral("<%1>").arg(email); } else { return QStringLiteral("(%2) <%1>").arg(email, comment); } } if (email.isEmpty()) { if (comment.isEmpty()) { return name; } else { return QStringLiteral("%1 (%2)").arg(name, comment); } } if (comment.isEmpty()) { return QStringLiteral("%1 <%2>").arg(name, email); } else { return QStringLiteral("%1 (%3) <%2>").arg(name, email, comment); } } if (proto == CMS) { const DN subject(id); const QString cn = subject[QStringLiteral("CN")].trimmed(); if (cn.isEmpty()) { return subject.prettyDN(); } return cn; } return QString(); } QString Formatting::prettyUserID(const UserID &uid) { if (uid.parent().protocol() == OpenPGP) { return prettyNameAndEMail(uid); } const QByteArray id = QByteArray(uid.id()).trimmed(); if (id.startsWith('<')) { return prettyEMail(uid.email(), uid.id()); } if (id.startsWith('(')) // ### parse uri/dns: { return QString::fromUtf8(uid.id()); } else { return DN(uid.id()).prettyDN(); } } QString Formatting::prettyKeyID(const char *id) { if (!id) { return QString(); } return QLatin1String("0x") + QString::fromLatin1(id).toUpper(); } QString Formatting::prettyNameAndEMail(const UserID &uid) { return prettyNameAndEMail(uid.parent().protocol(), uid.id(), uid.name(), uid.email(), uid.comment()); } QString Formatting::prettyNameAndEMail(const Key &key) { return prettyNameAndEMail(key.userID(0)); } QString Formatting::prettyName(const Key &key) { return prettyName(key.userID(0)); } QString Formatting::prettyName(const UserID &uid) { return prettyName(uid.parent().protocol(), uid.id(), uid.name(), uid.comment()); } QString Formatting::prettyName(const UserID::Signature &sig) { return prettyName(OpenPGP, sig.signerUserID(), sig.signerName(), sig.signerComment()); } // // EMail // QString Formatting::prettyEMail(const Key &key) { for (unsigned int i = 0, end = key.numUserIDs(); i < end; ++i) { const QString email = prettyEMail(key.userID(i)); if (!email.isEmpty()) { return email; } } return QString(); } QString Formatting::prettyEMail(const UserID &uid) { return prettyEMail(uid.email(), uid.id()); } QString Formatting::prettyEMail(const UserID::Signature &sig) { return prettyEMail(sig.signerEmail(), sig.signerUserID()); } QString Formatting::prettyEMail(const char *email_, const char *id) { QString email, name, comment; if (email_ && KEmailAddress::splitAddress(QString::fromUtf8(email_), name, email, comment) == KEmailAddress::AddressOk) { return email; } else { return DN(id)[QStringLiteral("EMAIL")].trimmed(); } } // // Tooltip // namespace { static QString protect_whitespace(QString s) { static const QLatin1Char SP(' '), NBSP('\xA0'); return s.replace(SP, NBSP); } template QString format_row(const QString &field, const T_arg &arg) { return QStringLiteral("%1:%2").arg(protect_whitespace(field), arg); } QString format_row(const QString &field, const QString &arg) { return QStringLiteral("%1:%2").arg(protect_whitespace(field), arg.toHtmlEscaped()); } QString format_row(const QString &field, const char *arg) { return format_row(field, QString::fromUtf8(arg)); } QString format_keytype(const Key &key) { const Subkey subkey = key.subkey(0); if (key.hasSecret()) { return i18n("%1-bit %2 (secret key available)", subkey.length(), QLatin1String(subkey.publicKeyAlgorithmAsString())); } else { return i18n("%1-bit %2", subkey.length(), QLatin1String(subkey.publicKeyAlgorithmAsString())); } } QString format_subkeytype(const Subkey &subkey) { const auto algo = subkey.publicKeyAlgorithm(); if (algo == Subkey::AlgoECC || algo == Subkey::AlgoECDSA || algo == Subkey::AlgoECDH || algo == Subkey::AlgoEDDSA) { return QString::fromStdString(subkey.algoName()); } return i18n("%1-bit %2", subkey.length(), QLatin1String(subkey.publicKeyAlgorithmAsString())); } QString format_keyusage(const Key &key) { QStringList capabilities; if (key.canReallySign()) { if (key.isQualified()) { capabilities.push_back(i18n("Signing (Qualified)")); } else { capabilities.push_back(i18n("Signing")); } } if (key.canEncrypt()) { capabilities.push_back(i18n("Encryption")); } if (key.canCertify()) { capabilities.push_back(i18n("Certifying User-IDs")); } if (key.canAuthenticate()) { capabilities.push_back(i18n("SSH Authentication")); } return capabilities.join(QLatin1String(", ")); } QString format_subkeyusage(const Subkey &subkey) { QStringList capabilities; if (subkey.canSign()) { if (subkey.isQualified()) { capabilities.push_back(i18n("Signing (Qualified)")); } else { capabilities.push_back(i18n("Signing")); } } if (subkey.canEncrypt()) { capabilities.push_back(i18n("Encryption")); } if (subkey.canCertify()) { capabilities.push_back(i18n("Certifying User-IDs")); } if (subkey.canAuthenticate()) { capabilities.push_back(i18n("SSH Authentication")); } return capabilities.join(QLatin1String(", ")); } static QString time_t2string(time_t t) { QDateTime dt; dt.setTime_t(t); return QLocale().toString(dt, QLocale::ShortFormat); } static QString make_red(const QString &txt) { return QLatin1String("") + txt.toHtmlEscaped() + QLatin1String(""); } } QString Formatting::toolTip(const Key &key, int flags) { if (flags == 0 || (key.protocol() != CMS && key.protocol() != OpenPGP)) { return QString(); } const Subkey subkey = key.subkey(0); QString result; if (flags & Validity) { if (key.protocol() == OpenPGP || (key.keyListMode() & Validate)) { if (key.isRevoked()) { result = make_red(i18n("Revoked")); } else if (key.isExpired()) { result = make_red(i18n("Expired")); } else if (key.isDisabled()) { result = i18n("Disabled"); } else if (key.keyListMode() & GpgME::Validate) { unsigned int fullyTrusted = 0; for (const auto &uid: key.userIDs()) { if (uid.validity() >= UserID::Validity::Full) { fullyTrusted++; } } if (fullyTrusted == key.numUserIDs()) { result = i18n("All User-IDs are certified."); const auto compliance = complianceStringForKey(key); if (!compliance.isEmpty()) { result += QStringLiteral("
") + compliance; } } else { result = i18np("One User-ID is not certified.", "%1 User-IDs are not certified.", key.numUserIDs() - fullyTrusted); } } else { result = i18n("The validity cannot be checked at the moment."); } } else { result = i18n("The validity cannot be checked at the moment."); } } if (flags == Validity) { return result; } result += QLatin1String(""); if (key.protocol() == CMS) { if (flags & SerialNumber) { result += format_row(i18n("Serial number"), key.issuerSerial()); } if (flags & Issuer) { result += format_row(i18n("Issuer"), key.issuerName()); } } if (flags & UserIDs) { const std::vector uids = key.userIDs(); if (!uids.empty()) result += format_row(key.protocol() == CMS ? i18n("Subject") : i18n("User-ID"), prettyUserID(uids.front())); if (uids.size() > 1) for (auto it = uids.begin() + 1, end = uids.end(); it != end; ++it) if (!it->isRevoked() && !it->isInvalid()) { result += format_row(i18n("a.k.a."), prettyUserID(*it)); } } if (flags & ExpiryDates) { result += format_row(i18n("Created"), time_t2string(subkey.creationTime())); if (key.isExpired()) { result += format_row(i18n("Expired"), time_t2string(subkey.expirationTime())); } else if (!subkey.neverExpires()) { result += format_row(i18n("Expires"), time_t2string(subkey.expirationTime())); } } if (flags & CertificateType) { result += format_row(i18n("Type"), format_keytype(key)); } if (flags & CertificateUsage) { result += format_row(i18n("Usage"), format_keyusage(key)); } if (flags & KeyID) { result += format_row(i18n("Key-ID"), QString::fromLatin1(key.shortKeyID())); } if (flags & Fingerprint) { result += format_row(i18n("Fingerprint"), key.primaryFingerprint()); } if (flags & OwnerTrust) { if (key.protocol() == OpenPGP) { result += format_row(i18n("Certification trust"), ownerTrustShort(key)); } else if (key.isRoot()) { result += format_row(i18n("Trusted issuer?"), key.userID(0).validity() == UserID::Ultimate ? i18n("Yes") : /* else */ i18n("No")); } } if (flags & StorageLocation) { if (const char *card = subkey.cardSerialNumber()) { result += format_row(i18n("Stored"), i18nc("stored...", "on SmartCard with serial no. %1", QString::fromUtf8(card))); } else { result += format_row(i18n("Stored"), i18nc("stored...", "on this computer")); } } if (flags & Subkeys) { for (const auto &sub: key.subkeys()) { result += QLatin1String("
"); result += format_row(i18n("Subkey"), sub.fingerprint()); if (sub.isRevoked()) { result += format_row(i18n("Status"), i18n("Revoked")); } else if (sub.isExpired()) { result += format_row(i18n("Status"), i18n("Expired")); } if (flags & ExpiryDates) { result += format_row(i18n("Created"), time_t2string(sub.creationTime())); if (key.isExpired()) { result += format_row(i18n("Expired"), time_t2string(sub.expirationTime())); } else if (!subkey.neverExpires()) { result += format_row(i18n("Expires"), time_t2string(sub.expirationTime())); } } if (flags & CertificateType) { result += format_row(i18n("Type"), format_subkeytype(sub)); } if (flags & CertificateUsage) { result += format_row(i18n("Usage"), format_subkeyusage(sub)); } if (flags & StorageLocation) { if (const char *card = sub.cardSerialNumber()) { result += format_row(i18n("Stored"), i18nc("stored...", "on SmartCard with serial no. %1", QString::fromUtf8(card))); } else { result += format_row(i18n("Stored"), i18nc("stored...", "on this computer")); } } } } result += QLatin1String("
"); return result; } namespace { template QString getValidityStatement(const Container &keys) { const bool allKeysAreOpenPGP = std::all_of(keys.cbegin(), keys.cend(), [] (const Key &key) { return key.protocol() == OpenPGP; }); const bool allKeysAreValidated = std::all_of(keys.cbegin(), keys.cend(), [] (const Key &key) { return key.keyListMode() & Validate; }); if (allKeysAreOpenPGP || allKeysAreValidated) { const bool someKeysAreBad = std::any_of(keys.cbegin(), keys.cend(), std::mem_fn(&Key::isBad)); if (someKeysAreBad) { return i18n("Some keys are revoked, expired, disabled, or invalid."); } else { const bool allKeysAreFullyValid = std::all_of(keys.cbegin(), keys.cend(), &Formatting::uidsHaveFullValidity); if (allKeysAreFullyValid) { return i18n("All keys are certified."); } else { return i18n("Some keys are not certified."); } } } return i18n("The validity of the keys cannot be checked at the moment."); } } QString Formatting::toolTip(const KeyGroup &group, int flags) { static const unsigned int maxNumKeysForTooltip = 20; if (group.isNull()) { return QString(); } const KeyGroup::Keys &keys = group.keys(); if (keys.size() == 0) { return i18nc("@info:tooltip", "This group does not contain any keys."); } const QString validity = (flags & Validity) ? getValidityStatement(keys) : QString(); if (flags == Validity) { return validity; } // list either up to maxNumKeysForTooltip keys or (maxNumKeysForTooltip-1) keys followed by "and n more keys" const unsigned int numKeysForTooltip = keys.size() > maxNumKeysForTooltip ? maxNumKeysForTooltip - 1 : keys.size(); QStringList result; result.reserve(3 + 2 + numKeysForTooltip + 2); if (!validity.isEmpty()) { result.push_back(QStringLiteral("

")); result.push_back(validity.toHtmlEscaped()); result.push_back(QStringLiteral("

")); } result.push_back(QStringLiteral("

")); result.push_back(i18n("Keys:")); { auto it = keys.cbegin(); for (unsigned int i = 0; i < numKeysForTooltip; ++i, ++it) { result.push_back(QLatin1String("
") + Formatting::summaryLine(*it).toHtmlEscaped()); } } if (keys.size() > numKeysForTooltip) { result.push_back(QLatin1String("
") + i18ncp("this follows a list of keys", "and 1 more key", "and %1 more keys", keys.size() - numKeysForTooltip)); } result.push_back(QStringLiteral("

")); return result.join(QLatin1Char('\n')); } // // Creation and Expiration // namespace { static QDate time_t2date(time_t t) { if (!t) { return {}; } QDateTime dt; dt.setTime_t(t); return dt.date(); } static QString date2string(const QDate &date) { return QLocale().toString(date, QLocale::ShortFormat); } template QString expiration_date_string(const T &tee) { return tee.neverExpires() ? QString() : date2string(time_t2date(tee.expirationTime())); } template QDate creation_date(const T &tee) { return time_t2date(tee.creationTime()); } template QDate expiration_date(const T &tee) { return time_t2date(tee.expirationTime()); } } QString Formatting::dateString(time_t t) { return date2string(time_t2date(t)); } QString Formatting::expirationDateString(const Key &key) { return expiration_date_string(key.subkey(0)); } QString Formatting::expirationDateString(const Subkey &subkey) { return expiration_date_string(subkey); } QString Formatting::expirationDateString(const UserID::Signature &sig) { return expiration_date_string(sig); } QDate Formatting::expirationDate(const Key &key) { return expiration_date(key.subkey(0)); } QDate Formatting::expirationDate(const Subkey &subkey) { return expiration_date(subkey); } QDate Formatting::expirationDate(const UserID::Signature &sig) { return expiration_date(sig); } QString Formatting::creationDateString(const Key &key) { return date2string(creation_date(key.subkey(0))); } QString Formatting::creationDateString(const Subkey &subkey) { return date2string(creation_date(subkey)); } QString Formatting::creationDateString(const UserID::Signature &sig) { return date2string(creation_date(sig)); } QDate Formatting::creationDate(const Key &key) { return creation_date(key.subkey(0)); } QDate Formatting::creationDate(const Subkey &subkey) { return creation_date(subkey); } QDate Formatting::creationDate(const UserID::Signature &sig) { return creation_date(sig); } // // Types // QString Formatting::displayName(Protocol p) { if (p == CMS) { return i18nc("X.509/CMS encryption standard", "S/MIME"); } if (p == OpenPGP) { return i18n("OpenPGP"); } return i18nc("Unknown encryption protocol", "Unknown"); } QString Formatting::type(const Key &key) { return displayName(key.protocol()); } QString Formatting::type(const Subkey &subkey) { return QString::fromUtf8(subkey.publicKeyAlgorithmAsString()); } QString Formatting::type(const KeyGroup &group) { Q_UNUSED(group) return i18nc("a group of keys/certificates", "Group"); } // // Status / Validity // QString Formatting::ownerTrustShort(const Key &key) { return ownerTrustShort(key.ownerTrust()); } QString Formatting::ownerTrustShort(Key::OwnerTrust trust) { switch (trust) { case Key::Unknown: return i18nc("unknown trust level", "unknown"); case Key::Never: return i18n("untrusted"); case Key::Marginal: return i18nc("marginal trust", "marginal"); case Key::Full: return i18nc("full trust", "full"); case Key::Ultimate: return i18nc("ultimate trust", "ultimate"); case Key::Undefined: return i18nc("undefined trust", "undefined"); default: Q_ASSERT(!"unexpected owner trust value"); break; } return QString(); } QString Formatting::validityShort(const Subkey &subkey) { if (subkey.isRevoked()) { return i18n("revoked"); } if (subkey.isExpired()) { return i18n("expired"); } if (subkey.isDisabled()) { return i18n("disabled"); } if (subkey.isInvalid()) { return i18n("invalid"); } return i18nc("as in good/valid signature", "good"); } QString Formatting::validityShort(const UserID &uid) { if (uid.isRevoked()) { return i18n("revoked"); } if (uid.isInvalid()) { return i18n("invalid"); } switch (uid.validity()) { case UserID::Unknown: return i18nc("unknown trust level", "unknown"); case UserID::Undefined: return i18nc("undefined trust", "undefined"); case UserID::Never: return i18n("untrusted"); case UserID::Marginal: return i18nc("marginal trust", "marginal"); case UserID::Full: return i18nc("full trust", "full"); case UserID::Ultimate: return i18nc("ultimate trust", "ultimate"); } return QString(); } QString Formatting::validityShort(const UserID::Signature &sig) { switch (sig.status()) { case UserID::Signature::NoError: if (!sig.isInvalid()) { /* See RFC 4880 Section 5.2.1 */ switch (sig.certClass()) { case 0x10: /* Generic */ case 0x11: /* Persona */ case 0x12: /* Casual */ case 0x13: /* Positive */ return i18n("valid"); case 0x30: return i18n("revoked"); default: return i18n("class %1", sig.certClass()); } } Q_FALLTHROUGH(); // fall through: case UserID::Signature::GeneralError: return i18n("invalid"); case UserID::Signature::SigExpired: return i18n("expired"); case UserID::Signature::KeyExpired: return i18n("certificate expired"); case UserID::Signature::BadSignature: return i18nc("fake/invalid signature", "bad"); case UserID::Signature::NoPublicKey: { /* GnuPG returns the same error for no public key as for expired * or revoked certificates. */ const auto key = KeyCache::instance()->findByKeyIDOrFingerprint (sig.signerKeyID()); if (key.isNull()) { return i18n("no public key"); } else if (key.isExpired()) { return i18n("key expired"); } else if (key.isRevoked()) { return i18n("key revoked"); } else if (key.isDisabled()) { return i18n("key disabled"); } /* can't happen */ return QStringLiteral("unknown"); } } return QString(); } QIcon Formatting::validityIcon(const UserID::Signature &sig) { switch (sig.status()) { case UserID::Signature::NoError: if (!sig.isInvalid()) { /* See RFC 4880 Section 5.2.1 */ switch (sig.certClass()) { case 0x10: /* Generic */ case 0x11: /* Persona */ case 0x12: /* Casual */ case 0x13: /* Positive */ return QIcon::fromTheme(QStringLiteral("emblem-success")); case 0x30: return QIcon::fromTheme(QStringLiteral("emblem-error")); default: return QIcon(); } } Q_FALLTHROUGH(); // fall through: case UserID::Signature::BadSignature: case UserID::Signature::GeneralError: return QIcon::fromTheme(QStringLiteral("emblem-error")); case UserID::Signature::SigExpired: case UserID::Signature::KeyExpired: return QIcon::fromTheme(QStringLiteral("emblem-information")); case UserID::Signature::NoPublicKey: return QIcon::fromTheme(QStringLiteral("emblem-question")); } return QIcon(); } QString Formatting::formatKeyLink(const Key &key) { if (key.isNull()) { return QString(); } return QStringLiteral("%2").arg(QLatin1String(key.primaryFingerprint()), Formatting::prettyName(key)); } QString Formatting::formatForComboBox(const GpgME::Key &key) { const QString name = prettyName(key); QString mail = prettyEMail(key); if (!mail.isEmpty()) { mail = QLatin1Char('<') + mail + QLatin1Char('>'); } return i18nc("name, email, key id", "%1 %2 (%3)", name, mail, QLatin1String(key.shortKeyID())).simplified(); } namespace { static QString keyToString(const Key &key) { Q_ASSERT(!key.isNull()); const QString email = Formatting::prettyEMail(key); const QString name = Formatting::prettyName(key); if (name.isEmpty()) { return email; } else if (email.isEmpty()) { return name; } else { return QStringLiteral("%1 <%2>").arg(name, email); } } } const char *Formatting::summaryToString(const Signature::Summary summary) { if (summary & Signature::Red) { return "RED"; } if (summary & Signature::Green) { return "GREEN"; } return "YELLOW"; } QString Formatting::signatureToString(const Signature &sig, const Key &key) { if (sig.isNull()) { return QString(); } const bool red = (sig.summary() & Signature::Red); const bool valid = (sig.summary() & Signature::Valid); if (red) if (key.isNull()) if (const char *fpr = sig.fingerprint()) { return i18n("Bad signature by unknown certificate %1: %2", QString::fromLatin1(fpr), QString::fromLocal8Bit(sig.status().asString())); } else { return i18n("Bad signature by an unknown certificate: %1", QString::fromLocal8Bit(sig.status().asString())); } else { return i18n("Bad signature by %1: %2", keyToString(key), QString::fromLocal8Bit(sig.status().asString())); } else if (valid) if (key.isNull()) if (const char *fpr = sig.fingerprint()) { return i18n("Good signature by unknown certificate %1.", QString::fromLatin1(fpr)); } else { return i18n("Good signature by an unknown certificate."); } else { return i18n("Good signature by %1.", keyToString(key)); } else if (key.isNull()) if (const char *fpr = sig.fingerprint()) { return i18n("Invalid signature by unknown certificate %1: %2", QString::fromLatin1(fpr), QString::fromLocal8Bit(sig.status().asString())); } else { return i18n("Invalid signature by an unknown certificate: %1", QString::fromLocal8Bit(sig.status().asString())); } else { return i18n("Invalid signature by %1: %2", keyToString(key), QString::fromLocal8Bit(sig.status().asString())); } } // // ImportResult // QString Formatting::importMetaData(const Import &import, const QStringList &ids) { const QString result = importMetaData(import); if (result.isEmpty()) { return QString(); } else return result + QLatin1Char('\n') + i18n("This certificate was imported from the following sources:") + QLatin1Char('\n') + ids.join(QLatin1Char('\n')); } QString Formatting::importMetaData(const Import &import) { if (import.isNull()) { return QString(); } if (import.error().isCanceled()) { return i18n("The import of this certificate was canceled."); } if (import.error()) return i18n("An error occurred importing this certificate: %1", QString::fromLocal8Bit(import.error().asString())); const unsigned int status = import.status(); if (status & Import::NewKey) return (status & Import::ContainedSecretKey) ? i18n("This certificate was new to your keystore. The secret key is available.") : i18n("This certificate is new to your keystore."); QStringList results; if (status & Import::NewUserIDs) { results.push_back(i18n("New user-ids were added to this certificate by the import.")); } if (status & Import::NewSignatures) { results.push_back(i18n("New signatures were added to this certificate by the import.")); } if (status & Import::NewSubkeys) { results.push_back(i18n("New subkeys were added to this certificate by the import.")); } return results.empty() ? i18n("The import contained no new data for this certificate. It is unchanged.") : results.join(QLatin1Char('\n')); } // // Overview in CertificateDetailsDialog // QString Formatting::formatOverview(const Key &key) { return toolTip(key, AllOptions); } QString Formatting::usageString(const Subkey &sub) { QStringList usageStrings; if (sub.canCertify()) { usageStrings << i18n("Certify"); } if (sub.canSign()) { usageStrings << i18n("Sign"); } if (sub.canEncrypt()) { usageStrings << i18n("Encrypt"); } if (sub.canAuthenticate()) { usageStrings << i18n("Authenticate"); } return usageStrings.join(QLatin1String(", ")); } QString Formatting::summaryLine(const Key &key) { return keyToString(key) + QLatin1Char(' ') + i18nc("(validity, protocol, creation date)", "(%1, %2, created: %3)", Formatting::complianceStringShort(key), displayName(key.protocol()), Formatting::creationDateString(key)); } QString Formatting::summaryLine(const KeyGroup &group) { switch (group.source()) { case KeyGroup::ApplicationConfig: case KeyGroup::GnuPGConfig: return i18ncp("name of group of keys (n key(s), validity)", "%2 (1 key, %3)", "%2 (%1 keys, %3)", group.keys().size(), group.name(), Formatting::complianceStringShort(group)); case KeyGroup::Tags: return i18ncp("name of group of keys (n key(s), validity, tag)", "%2 (1 key, %3, tag)", "%2 (%1 keys, %3, tag)", group.keys().size(), group.name(), Formatting::complianceStringShort(group)); default: return i18ncp("name of group of keys (n key(s), validity, group ...)", "%2 (1 key, %3, unknown origin)", "%2 (%1 keys, %3, unknown origin)", group.keys().size(), group.name(), Formatting::complianceStringShort(group)); } } namespace { QIcon iconForValidity(UserID::Validity validity) { switch (validity) { case UserID::Ultimate: case UserID::Full: case UserID::Marginal: return QIcon::fromTheme(QStringLiteral("emblem-success")); case UserID::Never: return QIcon::fromTheme(QStringLiteral("emblem-error")); case UserID::Undefined: case UserID::Unknown: default: return QIcon::fromTheme(QStringLiteral("emblem-information")); } } } // Icon for certificate selection indication QIcon Formatting::iconForUid(const UserID &uid) { return iconForValidity(uid.validity()); } QString Formatting::validity(const UserID &uid) { switch (uid.validity()) { case UserID::Ultimate: return i18n("The certificate is marked as your own."); case UserID::Full: return i18n("The certificate belongs to this recipient."); case UserID::Marginal: return i18n("The trust model indicates marginally that the certificate belongs to this recipient."); case UserID::Never: return i18n("This certificate should not be used."); case UserID::Undefined: case UserID::Unknown: default: return i18n("There is no indication that this certificate belongs to this recipient."); } } QString Formatting::validity(const KeyGroup &group) { if (group.isNull()) { return QString(); } const KeyGroup::Keys &keys = group.keys(); if (keys.size() == 0) { return i18n("This group does not contain any keys."); } return getValidityStatement(keys); } namespace { UserID::Validity minimalValidityOfNotRevokedUserIDs(const Key &key) { std::vector userIDs = key.userIDs(); const auto endOfNotRevokedUserIDs = std::remove_if(userIDs.begin(), userIDs.end(), std::mem_fn(&UserID::isRevoked)); const int minValidity = std::accumulate(userIDs.begin(), endOfNotRevokedUserIDs, UserID::Ultimate + 1, [] (int validity, const UserID &userID) { return std::min(validity, static_cast(userID.validity())); }); return minValidity <= UserID::Ultimate ? static_cast(minValidity) : UserID::Unknown; } template UserID::Validity minimalValidity(const Container& keys) { const int minValidity = std::accumulate(keys.cbegin(), keys.cend(), UserID::Ultimate + 1, [] (int validity, const Key &key) { return std::min(validity, minimalValidityOfNotRevokedUserIDs(key)); }); return minValidity <= UserID::Ultimate ? static_cast(minValidity) : UserID::Unknown; } } QIcon Formatting::validityIcon(const KeyGroup &group) { return iconForValidity(minimalValidity(group.keys())); } bool Formatting::uidsHaveFullValidity(const Key &key) { return minimalValidityOfNotRevokedUserIDs(key) >= UserID::Full; } QString Formatting::complianceMode() { const auto complianceValue = getCryptoConfigStringValue("gpg", "compliance"); return complianceValue == QLatin1String("gnupg") ? QString() : complianceValue; } bool Formatting::isKeyDeVs(const GpgME::Key &key) { for (const auto &sub: key.subkeys()) { if (sub.isExpired() || sub.isRevoked()) { // Ignore old subkeys continue; } if (!sub.isDeVs()) { return false; } } return true; } QString Formatting::complianceStringForKey(const GpgME::Key &key) { // There will likely be more in the future for other institutions // for now we only have DE-VS if (complianceMode() == QLatin1String("de-vs")) { if (uidsHaveFullValidity(key) && isKeyDeVs(key)) { return i18nc("%1 is a placeholder for the name of a compliance mode. E.g. NATO RESTRICTED compliant or VS-NfD compliant", "May be used for %1 communication.", deVsString()); } else { return i18nc("VS-NfD-conforming is a German standard for restricted documents. For which special restrictions about algorithms apply. The string describes if a key is compliant to that..", "May not be used for %1 communication.", deVsString()); } } return QString(); } QString Formatting::complianceStringShort(const GpgME::Key &key) { const bool keyValidityChecked = (key.keyListMode() & GpgME::Validate); if (keyValidityChecked && Formatting::uidsHaveFullValidity(key)) { if (complianceMode() == QLatin1String("de-vs") && Formatting::isKeyDeVs(key)) { return QStringLiteral("★ ") + deVsString(true); } return i18nc("As in all user IDs are valid.", "certified"); } if (key.isExpired()) { return i18n("expired"); } if (key.isRevoked()) { return i18n("revoked"); } if (key.isDisabled()) { return i18n("disabled"); } if (key.isInvalid()) { return i18n("invalid"); } if (keyValidityChecked) { return i18nc("As in not all user IDs are valid.", "not certified"); } return i18nc("The validity of the user IDs has not been/could not be checked", "not checked"); } QString Formatting::complianceStringShort(const KeyGroup &group) { const KeyGroup::Keys &keys = group.keys(); const bool allKeysFullyValid = std::all_of(keys.cbegin(), keys.cend(), &Formatting::uidsHaveFullValidity); if (allKeysFullyValid) { return i18nc("As in all keys are valid.", "all certified"); } return i18nc("As in not all keys are valid.", "not all certified"); } QString Formatting::prettyID(const char *id) { if (!id) { return QString(); } QString ret = QString::fromLatin1(id).toUpper().replace(QRegularExpression(QStringLiteral("(....)")), QStringLiteral("\\1 ")).trimmed(); // For the standard 10 group fingerprint let us use a double space in the // middle to increase readability if (ret.size() == 49) { ret.insert(24, QLatin1Char(' ')); } return ret; } QString Formatting::origin(int o) { switch (o) { case Key::OriginKS: return i18n("Keyserver"); case Key::OriginDane: return QStringLiteral("DANE"); case Key::OriginWKD: return QStringLiteral("WKD"); case Key::OriginURL: return QStringLiteral("URL"); case Key::OriginFile: return i18n("File import"); case Key::OriginSelf: return i18n("Generated"); case Key::OriginOther: case Key::OriginUnknown: default: return i18n("Unknown"); } } QString Formatting::deVsString(bool compliant) { const auto filter = KeyFilterManager::instance()->keyFilterByID(compliant ? QStringLiteral("de-vs-filter") : QStringLiteral("not-de-vs-filter")); if (!filter) { return compliant ? i18n("VS-NfD compliant") : i18n("Not VS-NfD compliant"); } return filter->name(); } namespace { QString formatTrustScope(const char *trustScope) { static const QRegularExpression escapedNonAlphaNum{QStringLiteral(R"(\\([^0-9A-Za-z]))")}; const auto scopeRegExp = QString::fromUtf8(trustScope); if (scopeRegExp.startsWith(u"<[^>]+[@.]") && scopeRegExp.endsWith(u">$")) { // looks like a trust scope regular expression created by gpg auto domain = scopeRegExp.mid(10, scopeRegExp.size() - 10 - 2); domain.replace(escapedNonAlphaNum, QStringLiteral(R"(\1)")); return domain; } return scopeRegExp; } } QString Formatting::trustSignatureDomain(const GpgME::UserID::Signature &sig) { #ifdef GPGMEPP_SUPPORTS_TRUST_SIGNATURES return formatTrustScope(sig.trustScope()); #else return {}; #endif } QString Formatting::trustSignature(const GpgME::UserID::Signature &sig) { #ifdef GPGMEPP_SUPPORTS_TRUST_SIGNATURES switch (sig.trustValue()) { case TrustSignatureTrust::Partial: return i18nc("Certifies this key as partially trusted introducer for 'domain name'.", "Certifies this key as partially trusted introducer for '%1'.", trustSignatureDomain(sig)); case TrustSignatureTrust::Complete: return i18nc("Certifies this key as fully trusted introducer for 'domain name'.", "Certifies this key as fully trusted introducer for '%1'.", trustSignatureDomain(sig)); default: return {}; } #else return {}; #endif }