diff --git a/src/models/keylistmodel.cpp b/src/models/keylistmodel.cpp index a6ee9e6f8..110e6f230 100644 --- a/src/models/keylistmodel.cpp +++ b/src/models/keylistmodel.cpp @@ -1,1230 +1,1230 @@ /* -*- 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/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); } 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 QString(); + return Formatting::complianceStringShort(group); case ValidFrom: return QString(); case ValidUntil: return QString(); case TechnicalDetails: - return i18nc("a group of keys/certificates", "Group"); + 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 QString(); + return Formatting::toolTip(group, toolTipOptions()); } else if (role == Qt::FontRole) { return QFont(); } else if (role == Qt::DecorationRole) { return column == Icon ? QIcon::fromTheme("group") : QVariant(); } else if (role == Qt::BackgroundRole) { } else if (role == Qt::ForegroundRole) { } else if (role == GroupRole) { return QVariant::fromValue(group); } return QVariant(); } 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; void doClear(ItemTypes types) override { if (types & Keys) { mKeysByFingerprint.clear(); } if (types & Groups) { mGroups.clear(); } } 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; void doClear(ItemTypes types) override { if (types & Keys) { mTopLevels.clear(); mKeysByFingerprint.clear(); mKeysByExistingParent.clear(); mKeysByNonExistingParent.clear(); } if (types & Groups) { mGroups.clear(); } } 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 = qBinaryFind(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 QString name = group.name(); const auto it = std::find_if(mGroups.begin(), mGroups.end(), [name](const KeyGroup &g) { return g.name() == name; }); if (it == mGroups.end()) { return QModelIndex(); } else { return createIndex(it - mGroups.begin() + 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(); } 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 = qBinaryFind(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 = qBinaryFind(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 = qBinaryFind(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 = qBinaryFind(oldKeys.begin(), oldKeys.end(), key, _detail::ByFingerprint()) != oldKeys.end(); 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 = qBinaryFind(last, mTopLevels.end(), k, _detail::ByFingerprint()); Q_ASSERT(last != mTopLevels.end()); const int row = std::distance(mTopLevels.begin(), last); lastFP = qBinaryFind(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 = qBinaryFind(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 = qBinaryFind(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 = qBinaryFind(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 = qBinaryFind(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 = qBinaryFind(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 QString name = group.name(); const auto it = std::find_if(mGroups.begin(), mGroups.end(), [name](const KeyGroup &g) { return g.name() == name; }); if (it == mGroups.end()) { return QModelIndex(); } else { return createIndex(it - mGroups.begin() + 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(); } 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/utils/formatting.cpp b/src/utils/formatting.cpp index 9f0fc78a7..9a7fdb7fb 100644 --- a/src/utils/formatting.cpp +++ b/src/utils/formatting.cpp @@ -1,1067 +1,1166 @@ /* -*- 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-License-Identifier: GPL-2.0-or-later */ #include "formatting.h" #include "kleo/dn.h" #include "kleo/keyfiltermanager.h" #include "kleo/keygroup.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 { 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."); } } 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 +{ +QString calculateValidity(const std::vector &keys) +{ + Q_ASSERT(keys.size() > 0); + + 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 std::vector &keys = group.keys(); + if (keys.size() == 0) { + return i18nc("@info:tooltip", "This group does not contain any keys."); + } + + const QString validity = (flags & Validity) ? calculateValidity(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("

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

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

"); + result.push_back(i18n("Keys:")); + for (unsigned int i = 0; i < numKeysForTooltip; ++i) { + result.push_back(QLatin1String("
") + Formatting::summaryLine(keys[i]).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("

"); + + 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) { return i18nc("name of group (group)", "%1 (group)", group.name()); } // Icon for certificate selection indication QIcon Formatting::iconForUid(const UserID &uid) { switch (uid.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")); } } 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 std::vector &keys = group.keys(); + if (keys.size() == 0) { + return i18n("This group does not contain any keys."); + } + + return calculateValidity(keys); +} + bool Formatting::uidsHaveFullValidity(const GpgME::Key &key) { bool oneValid = false; for (const auto &uid: key.userIDs()) { if (uid.isRevoked()) { /* Skip revoked uids */ continue; } if (uid.validity() < UserID::Validity::Full) { return false; } /* Only return true if we have found at least one * valid uid. E.g. if all uids are revoked we do * not want to return true here. */ oneValid = true; } return oneValid; } QString Formatting::complianceMode() { const QGpgME::CryptoConfig *const config = QGpgME::cryptoConfig(); if (!config) { return QString(); } const QGpgME::CryptoConfigEntry *const entry = config->entry(QStringLiteral("gpg"), QStringLiteral("Configuration"), QStringLiteral("compliance")); if (!entry || entry->stringValue() == QLatin1String("gnupg")) { return QString(); } return entry->stringValue(); } 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) { if (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"); } return i18nc("As in not all user IDs are valid.", "not certified"); } +QString Formatting::complianceStringShort(const KeyGroup &group) +{ + const std::vector &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(); } diff --git a/src/utils/formatting.h b/src/utils/formatting.h index dbaf1ff76..329016472 100644 --- a/src/utils/formatting.h +++ b/src/utils/formatting.h @@ -1,155 +1,159 @@ /* -*- mode: c++; c-basic-offset:4 -*- utils/formatting.h This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #ifndef __KLEOPATRA_UTILS_FORMATTING_H__ #define __KLEOPATRA_UTILS_FORMATTING_H__ #include #include class QString; #include class QDate; class QIcon; namespace GpgME { class Import; } namespace Kleo { class KeyGroup; namespace Formatting { KLEO_EXPORT QString prettyNameAndEMail(int proto, const char *id, const char *name, const char *email, const char *comment); KLEO_EXPORT QString prettyNameAndEMail(int proto, const QString &id, const QString &name, const QString &email, const QString &comment); KLEO_EXPORT QString prettyNameAndEMail(const GpgME::Key &key); KLEO_EXPORT QString prettyNameAndEMail(const GpgME::UserID &key); KLEO_EXPORT QString prettyUserID(const GpgME::UserID &uid); KLEO_EXPORT QString prettyKeyID(const char *id); KLEO_EXPORT QString prettyName(int proto, const char *id, const char *name, const char *comment); KLEO_EXPORT QString prettyName(const GpgME::Key &key); KLEO_EXPORT QString prettyName(const GpgME::UserID &uid); KLEO_EXPORT QString prettyName(const GpgME::UserID::Signature &sig); KLEO_EXPORT QString prettyEMail(const char *email, const char *id); KLEO_EXPORT QString prettyEMail(const GpgME::Key &key); KLEO_EXPORT QString prettyEMail(const GpgME::UserID &uid); KLEO_EXPORT QString prettyEMail(const GpgME::UserID::Signature &sig); /* Formats a fingerprint or keyid into groups of four */ KLEO_EXPORT QString prettyID(const char *id); enum ToolTipOption { KeyID = 0x001, Validity = 0x002, StorageLocation = 0x004, SerialNumber = 0x008, Issuer = 0x010, Subject = 0x020, ExpiryDates = 0x040, CertificateType = 0x080, CertificateUsage = 0x100, Fingerprint = 0x200, UserIDs = 0x400, OwnerTrust = 0x800, Subkeys = 0x1000, AllOptions = 0xffff }; KLEO_EXPORT QString toolTip(const GpgME::Key &key, int opts); +KLEO_EXPORT QString toolTip(const Kleo::KeyGroup &group, int opts); KLEO_EXPORT QString expirationDateString(const GpgME::Key &key); KLEO_EXPORT QString expirationDateString(const GpgME::Subkey &subkey); KLEO_EXPORT QString expirationDateString(const GpgME::UserID::Signature &sig); KLEO_EXPORT QDate expirationDate(const GpgME::Key &key); KLEO_EXPORT QDate expirationDate(const GpgME::Subkey &subkey); KLEO_EXPORT QDate expirationDate(const GpgME::UserID::Signature &sig); KLEO_EXPORT QString creationDateString(const GpgME::Key &key); KLEO_EXPORT QString creationDateString(const GpgME::Subkey &subkey); KLEO_EXPORT QString creationDateString(const GpgME::UserID::Signature &sig); KLEO_EXPORT QDate creationDate(const GpgME::Key &key); KLEO_EXPORT QDate creationDate(const GpgME::Subkey &subkey); KLEO_EXPORT QDate creationDate(const GpgME::UserID::Signature &sig); /* Convert a GPGME style time to a localized string */ KLEO_EXPORT QString dateString(time_t t); KLEO_EXPORT QString displayName(GpgME::Protocol prot); KLEO_EXPORT QString type(const GpgME::Key &key); KLEO_EXPORT QString type(const GpgME::Subkey &subkey); +KLEO_EXPORT QString type(const Kleo::KeyGroup &group); KLEO_EXPORT QString ownerTrustShort(const GpgME::Key &key); KLEO_EXPORT QString ownerTrustShort(GpgME::Key::OwnerTrust trust); KLEO_EXPORT QString validityShort(const GpgME::Subkey &subkey); KLEO_EXPORT QString validityShort(const GpgME::UserID &uid); KLEO_EXPORT QString validityShort(const GpgME::UserID::Signature &sig); KLEO_EXPORT QIcon validityIcon(const GpgME::UserID::Signature &sig); /* A sentence about the validity of the UserID */ KLEO_EXPORT QString validity(const GpgME::UserID &uid); +KLEO_EXPORT QString validity(const Kleo::KeyGroup &group); KLEO_EXPORT QString formatForComboBox(const GpgME::Key &key); KLEO_EXPORT QString formatKeyLink(const GpgME::Key &key); KLEO_EXPORT QString signatureToString(const GpgME::Signature &sig, const GpgME::Key &key); KLEO_EXPORT const char *summaryToString(const GpgME::Signature::Summary summary); KLEO_EXPORT QString importMetaData(const GpgME::Import &import); KLEO_EXPORT QString importMetaData(const GpgME::Import &import, const QStringList &sources); KLEO_EXPORT QString formatOverview(const GpgME::Key &key); KLEO_EXPORT QString usageString(const GpgME::Subkey &subkey); KLEO_EXPORT QString summaryLine(const GpgME::Key &key); KLEO_EXPORT QString summaryLine(const KeyGroup &group); KLEO_EXPORT QIcon iconForUid(const GpgME::UserID &uid); /* Is the key valid i.e. are all uids fully trusted? */ KLEO_EXPORT bool uidsHaveFullValidity(const GpgME::Key &key); /* The compliance mode of the gnupg system. Empty if compliance * mode is not set. */ KLEO_EXPORT QString complianceMode(); /* Is the given key in compliance with CO_DE_VS? */ KLEO_EXPORT bool isKeyDeVs(const GpgME::Key &key); /* Localized string describing the name of the VS-NfD Compliance filter. If * compliant is false the name of the not Compliant filter. * * This is required to make the string configurable which is * a common request from users because VS-NfD compliance is called * differently in different enviornments. E.g NATO RESTRICTED or * EU RESTRICTED. */ KLEO_EXPORT QString deVsString (bool compliant = true); /* A sentence if the key confirms to the current compliance mode */ KLEO_EXPORT QString complianceStringForKey(const GpgME::Key &key); /* A single word for use in keylists to describe the validity of the * given key, including any conformance statements relevant to the * current conformance mode. */ KLEO_EXPORT QString complianceStringShort(const GpgME::Key &key); +KLEO_EXPORT QString complianceStringShort(const Kleo::KeyGroup &group); /* The origin of the key mapped to a localized string */ KLEO_EXPORT QString origin(int o); } } #endif /* __KLEOPATRA_UTILS_FORMATTING_H__ */