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 }