File indexing completed on 2024-04-21 05:50:40

0001 /*
0002     SPDX-FileCopyrightText: 2009-2022 Rolf Eike Beer <kde@opensource.sf-tec.de>
0003     SPDX-FileCopyrightText: 2016 David Zaslavsky <diazona@ellipsix.net>
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "kgpgsearchresultmodel.h"
0008 #include "kgpg_general_debug.h"
0009 
0010 #include <KLocalizedString>
0011 
0012 
0013 #include <QDateTime>
0014 #include <QRegularExpression>
0015 #include <QScopedPointer>
0016 #include <QString>
0017 #include <QTextCodec>
0018 
0019 #include "core/convert.h"
0020 #include "core/kgpgkey.h"
0021 
0022 class SearchResult {
0023 private:
0024     QStringList m_emails;
0025     QStringList m_names;
0026 
0027 public:
0028     explicit SearchResult(const QString &line);
0029 
0030     bool m_validPub;    // true when the "pub" line passed to constructor was valid
0031 
0032     void addUid(const QString &id);
0033     const QString &getName(const int index) const;
0034     const QString &getEmail(const int index) const;
0035     int getUidCount() const;
0036     bool valid() const;
0037     bool expired() const;
0038     bool revoked() const;
0039 
0040     QString m_fingerprint;
0041     unsigned int m_uatCount;
0042 
0043     QVariant summary() const;
0044 private:
0045     QDateTime m_creation;
0046     bool m_expired;
0047     bool m_revoked;
0048     unsigned int m_bits;
0049     KgpgCore::KgpgKeyAlgo m_algo;
0050 };
0051 
0052 class KGpgSearchResultModelPrivate {
0053 public:
0054     explicit KGpgSearchResultModelPrivate() = default;
0055     ~KGpgSearchResultModelPrivate();
0056 
0057     QList<SearchResult *> m_items;
0058 
0059     QString urlDecode(const QString &line);
0060 };
0061 
0062 SearchResult::SearchResult(const QString &line)
0063     : m_validPub(false),
0064     m_uatCount(0),
0065     m_expired(false),
0066     m_revoked(false),
0067     m_bits(0)
0068 {
0069     const QStringList parts(line.split(QLatin1Char( ':' )));
0070 
0071     if (parts.count() < 6)
0072         return;
0073 
0074     if (parts.at(1).isEmpty())
0075         return;
0076 
0077     m_fingerprint = parts.at(1);
0078     m_algo = KgpgCore::Convert::toAlgo(parts.at(2));
0079     m_bits = parts.at(3).toUInt();
0080     m_creation.setSecsSinceEpoch(parts.at(4).toULongLong());
0081     m_expired = !parts.at(5).isEmpty() &&
0082             QDateTime::fromSecsSinceEpoch(parts.at(5).toULongLong()) <= QDateTime::currentDateTimeUtc();
0083     m_revoked = (parts.at(6) == QLatin1String( "r" ));
0084 
0085     m_validPub = true;
0086 }
0087 
0088 void
0089 SearchResult::addUid(const QString &id)
0090 {
0091     Q_ASSERT(m_emails.count() == m_names.count());
0092     const QRegularExpression hasmail(QRegularExpression::anchoredPattern(QStringLiteral("(.*) <(.*)>")));
0093 
0094     const QRegularExpressionMatch match = hasmail.match(id);
0095     if (match.hasMatch()) {
0096         m_names.append(match.captured(1));
0097         m_emails.append(match.captured(2));
0098     } else {
0099         m_names.append(id);
0100         m_emails.append(QString());
0101     }
0102 }
0103 
0104 const QString &
0105 SearchResult::getName(const int index) const
0106 {
0107     return m_names.at(index);
0108 }
0109 
0110 const QString &
0111 SearchResult::getEmail(const int index) const
0112 {
0113     return m_emails.at(index);
0114 }
0115 
0116 int
0117 SearchResult::getUidCount() const
0118 {
0119     Q_ASSERT(m_emails.count() == m_names.count());
0120 
0121     return m_emails.count();
0122 }
0123 
0124 bool
0125 SearchResult::expired() const
0126 {
0127     return m_expired;
0128 }
0129 
0130 bool
0131 SearchResult::revoked() const
0132 {
0133     return m_revoked;
0134 }
0135 
0136 bool
0137 SearchResult::valid() const
0138 {
0139     return !(revoked() || expired());
0140 }
0141 
0142 QVariant
0143 SearchResult::summary() const
0144 {
0145     if (m_revoked) {
0146         return i18nc("example: ID abc123xy, 1024-bit RSA key, created Jan 12 2009, revoked",
0147                 "ID %1, %2-bit %3 key, created %4, revoked", m_fingerprint,
0148                 m_bits, KgpgCore::Convert::toString(m_algo),
0149                 QLocale::system().toString(m_creation, QLocale::ShortFormat));
0150     } else {
0151         return i18nc("example: ID abc123xy, 1024-bit RSA key, created Jan 12 2009",
0152                 "ID %1, %2-bit %3 key, created %4", m_fingerprint,
0153                 m_bits, KgpgCore::Convert::toString(m_algo),
0154                 QLocale::system().toString(m_creation, QLocale::ShortFormat));
0155     }
0156 }
0157 
0158 KGpgSearchResultModelPrivate::~KGpgSearchResultModelPrivate()
0159 {
0160     qDeleteAll(m_items);
0161 }
0162 
0163 QString
0164 KGpgSearchResultModelPrivate::urlDecode(const QString &line)
0165 {
0166     if (!line.contains(QLatin1Char( '%' )))
0167         return line;
0168 
0169     QByteArray tmp(line.toLatin1());
0170     const QRegularExpression hex(
0171         QRegularExpression::anchoredPattern(QStringLiteral("[A-F0-9]{2}")));    // URL-encoding uses only uppercase
0172 
0173     int pos = -1;   // avoid error if '%' is URL-encoded
0174     while ((pos = tmp.indexOf("%", pos + 1)) >= 0) {
0175         const QByteArray hexnum(tmp.mid(pos + 1, 2));
0176 
0177         // the input is not properly URL-encoded, so assume it does not need to be decoded at all
0178         if (!hex.match(QLatin1String(hexnum)).hasMatch())
0179             return line;
0180 
0181         char n[2];
0182         // this must work as we checked the regexp before
0183         n[0] = hexnum.toUShort(nullptr, 16);
0184         n[1] = '\0';    // to use n as a 0-terminated string
0185 
0186         tmp.replace(pos, 3, n);
0187     }
0188 
0189     return QTextCodec::codecForName("utf8")->toUnicode(tmp);
0190 }
0191 
0192 KGpgSearchResultBackingModel::KGpgSearchResultBackingModel(QObject *parent)
0193     : QAbstractItemModel(parent), d(new KGpgSearchResultModelPrivate())
0194 {
0195 }
0196 
0197 KGpgSearchResultBackingModel::~KGpgSearchResultBackingModel()
0198 {
0199     delete d;
0200 }
0201 
0202 /*
0203  * In this implementation, the top-level node is identified by
0204  * an invalid `QModelIndex`. Sublevel nodes correspond to valid
0205  * `QModelIndex` instances. First-level nodes have a null `internalPointer`,
0206  * and the `SearchResult` that holds a key is stored as the `internalPointer`
0207  * of each second-level subnode under that key's first-level subnode.
0208  *
0209  * This design works better than storing pointers to the `SearchResult`s
0210  * in the first-level nodes because the second-level nodes need some way
0211  * to be linked to their parent nodes. Storing a pointer to the parent
0212  * `QModelIndex` in the second-level `QModelIndex` is tricky because of
0213  * the short lifetime of `QModelIndex` instances. However, it's
0214  * straightforward to get from a `SearchResult` to the corresponding
0215  * first-level `QModelIndex`, so effectively the `SearchResult` instances
0216  * do double duty as "proxy pointers" to first-level `QModelIndex`s,
0217  * which aren't going to disappear from memory at any moment.
0218  */
0219 
0220 KGpgSearchResultBackingModel::NodeLevel
0221 KGpgSearchResultBackingModel::nodeLevel(const QModelIndex &index)
0222 {
0223     if (!index.isValid())
0224         return ROOT_LEVEL;
0225     else if (index.internalPointer() == nullptr)
0226         return KEY_LEVEL;
0227     else
0228         return ATTRIBUTE_LEVEL;
0229 }
0230 
0231 SearchResult *
0232 KGpgSearchResultBackingModel::resultForIndex(const QModelIndex &index) const
0233 {
0234     switch (nodeLevel(index)) {
0235     case KEY_LEVEL:
0236         return d->m_items.at(index.row());
0237     case ATTRIBUTE_LEVEL:
0238     {
0239         SearchResult *tmp = static_cast<SearchResult *>(index.internalPointer());
0240         Q_ASSERT(tmp != nullptr);
0241         return tmp;
0242     }
0243     default:
0244         return nullptr;
0245     }
0246 }
0247 
0248 QVariant
0249 KGpgSearchResultBackingModel::data(const QModelIndex &index, int role) const
0250 {
0251     if (!index.isValid())
0252         return QVariant();
0253 
0254     if (role != Qt::DisplayRole)
0255         return QVariant();
0256 
0257     if (index.row() < 0)
0258         return QVariant();
0259 
0260     SearchResult *tmp = resultForIndex(index);
0261     int row;
0262 
0263     switch (nodeLevel(index)) {
0264     case KEY_LEVEL:
0265         // this is a "top" item, show the first uid
0266         if (index.row() >= d->m_items.count())
0267             return QVariant();
0268 
0269         row = 0;
0270         break;
0271     case ATTRIBUTE_LEVEL:
0272     {
0273         row = index.row() + 1;
0274         int summaryRow = tmp->getUidCount();
0275         int uatRow;
0276         if (tmp->m_uatCount != 0) {
0277             uatRow = summaryRow;
0278             summaryRow++;
0279         } else {
0280             uatRow = -1;
0281         }
0282 
0283         if (row == uatRow) {
0284             if (index.column() == 0)
0285                 return i18np("One Photo ID", "%1 Photo IDs", tmp->m_uatCount);
0286             else
0287                 return QVariant();
0288         } else if (row == summaryRow) {
0289             if (index.column() == 0)
0290                 return tmp->summary();
0291             else
0292                 return QVariant();
0293         }
0294         Q_ASSERT(row < tmp->getUidCount());
0295         break;
0296     }
0297     case ROOT_LEVEL:
0298         // The root index, level 0, should have been caught by the conditional
0299         // if (!index.isValid()) {...} at the top of this method
0300         Q_ASSERT(false);
0301         break;
0302     }
0303 
0304     switch (index.column()) {
0305     case 0:
0306         return tmp->getName(row);
0307     case 1:
0308         return tmp->getEmail(row);
0309     default:
0310         return QVariant();
0311     }
0312 }
0313 
0314 int
0315 KGpgSearchResultBackingModel::columnCount(const QModelIndex &parent) const
0316 {
0317     switch (nodeLevel(parent)) {
0318     case KEY_LEVEL:
0319         if (parent.column() != 0)
0320             return 0;
0321         else
0322             return 2;
0323     case ATTRIBUTE_LEVEL:
0324         return 0;
0325     case ROOT_LEVEL:
0326         return 2;
0327     default:
0328         Q_ASSERT(false);
0329         return 0;
0330     }
0331 }
0332 
0333 QModelIndex
0334 KGpgSearchResultBackingModel::index(int row, int column, const QModelIndex &parent) const
0335 {
0336     switch (nodeLevel(parent)) {
0337     case ATTRIBUTE_LEVEL:
0338         return QModelIndex();
0339     case KEY_LEVEL:
0340     {
0341         if (parent.row() >= d->m_items.count())
0342             return QModelIndex();
0343         SearchResult *tmp = resultForIndex(parent);
0344         int maxRow = tmp->getUidCount();
0345         if (tmp->m_uatCount != 0)
0346             maxRow++;
0347         if ((row >= maxRow) || (column > 1))
0348             return QModelIndex();
0349         return createIndex(row, column, tmp);
0350     }
0351     case ROOT_LEVEL:
0352         if ((row >= d->m_items.count()) || (column > 1) || (row < 0) || (column < 0))
0353             return QModelIndex();
0354         return createIndex(row, column);
0355     default:
0356         Q_ASSERT(false);
0357         return QModelIndex();
0358     }
0359 }
0360 
0361 QModelIndex
0362 KGpgSearchResultBackingModel::parent(const QModelIndex &index) const
0363 {
0364     if (!index.isValid())
0365         return QModelIndex();
0366 
0367     switch (nodeLevel(index)) {
0368     case ROOT_LEVEL:
0369     case KEY_LEVEL:
0370         return QModelIndex();
0371     case ATTRIBUTE_LEVEL:
0372     {
0373         SearchResult *tmp = resultForIndex(index);
0374         return createIndex(d->m_items.indexOf(tmp), 0);
0375     }
0376     default:
0377         Q_ASSERT(false);
0378         return QModelIndex();
0379     }
0380 }
0381 
0382 int
0383 KGpgSearchResultBackingModel::rowCount(const QModelIndex &parent) const
0384 {
0385     switch (nodeLevel(parent)) {
0386     case ROOT_LEVEL:
0387         return d->m_items.count();
0388     case KEY_LEVEL:
0389         if (parent.column() == 0) {
0390             SearchResult *item = resultForIndex(parent);
0391             int cnt = item->getUidCount();
0392             if (item->m_uatCount != 0)
0393                 cnt++;
0394 
0395             return cnt;
0396         } else {
0397             return 0;
0398         }
0399     case ATTRIBUTE_LEVEL:
0400         return 0;
0401     default:
0402         Q_ASSERT(false);
0403         return 0;
0404     }
0405 }
0406 
0407 QVariant
0408 KGpgSearchResultBackingModel::headerData(int section, Qt::Orientation orientation, int role) const
0409 {
0410     if (role != Qt::DisplayRole)
0411         return QVariant();
0412 
0413     if (orientation != Qt::Horizontal)
0414         return QVariant();
0415 
0416     switch (section) {
0417     case 0:
0418         return i18n("Name");
0419     case 1:
0420         return QString(i18nc("@title:column Title of a column of emails", "Email"));
0421     default:
0422         return QVariant();
0423     }
0424 }
0425 
0426 QString
0427 KGpgSearchResultBackingModel::idForIndex(const QModelIndex &index) const
0428 {
0429     Q_ASSERT(index.isValid());
0430 
0431     switch (nodeLevel(index)) {
0432     case KEY_LEVEL:
0433     case ATTRIBUTE_LEVEL:
0434         return resultForIndex(index)->m_fingerprint;
0435     default:
0436         // root level should have been caught at the beginning
0437         Q_ASSERT(false);
0438         return QString();
0439     }
0440 }
0441 
0442 void
0443 KGpgSearchResultBackingModel::slotAddKey(const QStringList &lines)
0444 {
0445     Q_ASSERT(!lines.isEmpty());
0446     Q_ASSERT(lines.first().startsWith(QLatin1String("pub:")));
0447 
0448     if (lines.count() == 1)
0449         return;
0450 
0451     QStringList::const_iterator it = lines.constBegin();
0452 
0453     auto nkey = std::make_unique<SearchResult>(*it);
0454     if (!nkey->m_validPub)
0455         return;
0456 
0457     const QStringList::const_iterator itEnd = lines.constEnd();
0458     for (it++; it != itEnd; it++) {
0459         const QString &line = *it;
0460         if (line.startsWith(QLatin1String("uid:"))) {
0461             QString kid = d->urlDecode(line.section(QLatin1Char( ':' ), 1, 1));
0462 
0463             nkey->addUid(kid);
0464         } else if (line.startsWith(QLatin1String("uat:"))) {
0465             nkey->m_uatCount++;
0466         } else {
0467             qCDebug(KGPG_LOG_GENERAL) << "ignored search result line" << line;
0468         }
0469     }
0470 
0471     if (nkey->getUidCount() > 0) {
0472         beginInsertRows(QModelIndex(), d->m_items.count(), d->m_items.count());
0473         d->m_items.append(nkey.release());
0474         endInsertRows();
0475     }
0476 }
0477 
0478 KGpgSearchResultModel::KGpgSearchResultModel(QObject *parent)
0479     : QSortFilterProxyModel(parent),
0480     m_filterByValidity(true)
0481 {
0482     resetSourceModel();
0483 }
0484 
0485 bool
0486 KGpgSearchResultModel::filterByValidity() const
0487 {
0488     return m_filterByValidity;
0489 }
0490 
0491 QString
0492 KGpgSearchResultModel::idForIndex(const QModelIndex &index) const
0493 {
0494     return static_cast<KGpgSearchResultBackingModel *>(sourceModel())->idForIndex(mapToSource(index));
0495 }
0496 
0497 int
0498 KGpgSearchResultModel::sourceRowCount(const QModelIndex &parent) const
0499 {
0500     return sourceModel()->rowCount(parent);
0501 }
0502 
0503 void
0504 KGpgSearchResultModel::setFilterByValidity(bool filter)
0505 {
0506     m_filterByValidity = filter;
0507     invalidateFilter();
0508 }
0509 
0510 void
0511 KGpgSearchResultModel::setSourceModel(QAbstractItemModel *)
0512 {
0513     Q_ASSERT(false);
0514 }
0515 
0516 void
0517 KGpgSearchResultModel::slotAddKey(const QStringList &key)
0518 {
0519     static_cast<KGpgSearchResultBackingModel *>(sourceModel())->slotAddKey(key);
0520 }
0521 
0522 void
0523 KGpgSearchResultModel::resetSourceModel()
0524 {
0525     QAbstractItemModel *oldSourceModel = sourceModel();
0526     if (oldSourceModel != nullptr)
0527         oldSourceModel->deleteLater();
0528     QSortFilterProxyModel::setSourceModel(new KGpgSearchResultBackingModel(this));
0529 }
0530 
0531 bool
0532 KGpgSearchResultModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
0533 {
0534     // first check the text filter, implemented in the superclass
0535     if (!QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent)) {
0536         return false;
0537     } else if (!filterByValidity()) {
0538         // if the text filter matched and we're not hiding invalid keys, accept the row
0539         return true;
0540     }
0541     // otherwise, validity filtering is enabled, so check whether the row is valid
0542     KGpgSearchResultBackingModel *backingModel = static_cast<KGpgSearchResultBackingModel *>(sourceModel());
0543     QModelIndex currentKeyIndex = backingModel->index(sourceRow, 0, sourceParent);
0544     return backingModel->resultForIndex(currentKeyIndex)->valid();
0545 }