File indexing completed on 2024-12-15 04:57:00
0001 // SPDX-FileCopyrightText: 2021 Nicolas Fella <nicolas.fella@gmx.de> 0002 // SPDX-License-Identifier: GPL-2.0-or-later 0003 0004 #include "certificatesmodel.h" 0005 0006 #include <KItinerary/ExtractorDocumentNode> 0007 #include <KItinerary/ExtractorEngine> 0008 #include <kitinerary_version.h> 0009 0010 #include <QClipboard> 0011 #include <QDebug> 0012 #include <QFile> 0013 #include <QGuiApplication> 0014 #include <QJsonArray> 0015 #include <QMimeData> 0016 0017 #include <KLocalizedString> 0018 0019 #include <KHealthCertificateParser> 0020 #include <khealthcertificate_version.h> 0021 0022 QVector<AnyCertificate> CertificatesModel::fromStringList(const QStringList rawCertificates) const 0023 { 0024 QVector<AnyCertificate> res; 0025 res.reserve(rawCertificates.size()); 0026 for (const auto &rawCertificate : rawCertificates) { 0027 const auto cert = parseCertificate(*QByteArray::fromBase64Encoding(rawCertificate.toUtf8())); 0028 if (cert) { 0029 res.push_back(*cert); 0030 } 0031 } 0032 return res; 0033 } 0034 0035 static QByteArray certRawData(const AnyCertificate &cert) 0036 { 0037 return std::visit( 0038 [](auto &&c) { 0039 return c.rawData(); 0040 }, 0041 cert); 0042 } 0043 0044 QStringList CertificatesModel::toStringList(const QVector<AnyCertificate> certificates) const 0045 { 0046 QStringList res; 0047 std::transform(certificates.cbegin(), certificates.cend(), std::back_inserter(res), [](const AnyCertificate &cert) { 0048 return QString::fromUtf8(certRawData(cert).toBase64()); 0049 }); 0050 return res; 0051 } 0052 0053 static QDateTime certRelevantUntil(const AnyCertificate &cert) 0054 { 0055 return std::visit( 0056 [](auto &&c) { 0057 return KHealthCertificate::relevantUntil(c); 0058 }, 0059 cert); 0060 } 0061 0062 static bool certLessThan(const AnyCertificate &lhs, const AnyCertificate &rhs) 0063 { 0064 const auto lhsDt = certRelevantUntil(lhs); 0065 const auto rhsDt = certRelevantUntil(rhs); 0066 if (lhsDt == rhsDt) { 0067 return certRawData(lhs) < certRawData(rhs); // ensure a stable sorting in all cases 0068 } 0069 if (!lhsDt.isValid()) { 0070 return false; 0071 } 0072 return !rhsDt.isValid() || lhsDt > rhsDt; 0073 } 0074 0075 const QByteArray sample = 0076 "HC1:NCF570.90T9WTWGVLKG99.+VKV9NT3RH1X*4%AB3XK4F36:G$MB2F3F*K+UR3JCYHAY50.FK6ZK7:EDOLFVCPD0B$D% " 0077 "D3IA4W5646946%96X476KCN9E%961A69L6QW6B46XJCCWENF6OF63W5Y96B46WJCT3E2+8WJC0FD4:473DSDDF+ANG7ZHAFM89A6A1A71B/M8RY971BS1BAC9$+ADB8ZCAM%6//6.JCP9EJY8L/5M/" 0078 "5546.96D46%JCIQE1C93KC.SC4KCD3DX47B46IL6646I*6..DX%DLPCG/DZ-CFZA71A1T8W.CZ-C4%E-3E4VCI3D7WEMY95IAWY8I3DD " 0079 "CGECQED$PC5$CUZCY$5Y$5JPCT3E5JDLA7KF6D463W5WA6%78%VIKQS*9OE.U37WGJG.1J5PF9WOASFU3UI69PKJEH2F:SY2SCYKFOMVGP OLGW31.J5OVSAFBGON19H+HCSIA7P:65P0F-QR/GS:2"; 0080 0081 CertificatesModel::CertificatesModel(bool testMode) 0082 : QAbstractListModel() 0083 , m_config(QStringLiteral("vakzinationrc")) 0084 , m_testMode(testMode) 0085 { 0086 m_generalConfig = m_config.group(QStringLiteral("General")); 0087 0088 if (m_testMode) { 0089 for (int i = 0; i < 4; i++) { 0090 m_certificates << KHealthCertificateParser::parse(sample).value<KVaccinationCertificate>(); 0091 } 0092 } else { 0093 const QStringList certs = m_generalConfig.readEntry(QStringLiteral("certificates"), QStringList{}); 0094 m_certificates = fromStringList(certs); 0095 } 0096 0097 std::sort(m_certificates.begin(), m_certificates.end(), certLessThan); 0098 } 0099 0100 QVariant CertificatesModel::data(const QModelIndex &index, int role) const 0101 { 0102 const int row = index.row(); 0103 if (!checkIndex(index) || row < 0) { 0104 return {}; 0105 } 0106 0107 switch (role) { 0108 case Qt::DisplayRole: { 0109 if (std::holds_alternative<KVaccinationCertificate>(m_certificates[row])) { 0110 const auto cert = std::get<KVaccinationCertificate>(m_certificates[row]); 0111 if (cert.dose() > 0 && cert.totalDoses() > 0) { 0112 return i18n("Vaccination %1/%2 (%3)", cert.dose(), cert.totalDoses(), cert.name()); 0113 } 0114 return i18n("Vaccination (%1)", cert.name()); 0115 } 0116 if (std::holds_alternative<KTestCertificate>(m_certificates[row])) { 0117 const auto cert = std::get<KTestCertificate>(m_certificates[row]); 0118 return i18n("Test %1 (%2)", 0119 QLocale().toString(cert.date().isValid() ? cert.date() : cert.certificateIssueDate().date(), QLocale::NarrowFormat), 0120 cert.name()); 0121 } 0122 if (std::holds_alternative<KRecoveryCertificate>(m_certificates[row])) { 0123 const auto cert = std::get<KRecoveryCertificate>(m_certificates[row]); 0124 return i18n("Recovery (%1)", cert.name()); 0125 } 0126 return {}; 0127 } 0128 case CertificateRole: { 0129 return std::visit( 0130 [](auto &&arg) -> QVariant { 0131 return arg; 0132 }, 0133 m_certificates[row]); 0134 } 0135 case TypeRole: { 0136 if (std::holds_alternative<KVaccinationCertificate>(m_certificates[row])) { 0137 return KHealthCertificate::Vaccination; 0138 } 0139 if (std::holds_alternative<KTestCertificate>(m_certificates[row])) { 0140 return KHealthCertificate::Test; 0141 } 0142 if (std::holds_alternative<KRecoveryCertificate>(m_certificates[row])) { 0143 return KHealthCertificate::Recovery; 0144 } 0145 } 0146 }; 0147 return {}; 0148 } 0149 0150 int CertificatesModel::rowCount(const QModelIndex &parent) const 0151 { 0152 Q_UNUSED(parent) 0153 return m_certificates.size(); 0154 } 0155 0156 QHash<int, QByteArray> CertificatesModel::roleNames() const 0157 { 0158 auto n = QAbstractListModel::roleNames(); 0159 n.insert(CertificateRole, "certificate"); 0160 n.insert(TypeRole, "type"); 0161 return n; 0162 } 0163 0164 void CertificatesModel::addCertificate(AnyCertificate cert) 0165 { 0166 auto it = std::find(m_certificates.begin(), m_certificates.end(), cert); 0167 if (it != m_certificates.end()) { // certificate is already known 0168 return; 0169 } 0170 0171 it = std::lower_bound(m_certificates.begin(), m_certificates.end(), cert, certLessThan); 0172 const auto row = std::distance(m_certificates.begin(), it); 0173 beginInsertRows({}, row, row); 0174 m_certificates.insert(it, cert); 0175 0176 if (!m_testMode) { 0177 m_generalConfig.writeEntry(QStringLiteral("certificates"), toStringList(m_certificates)); 0178 } 0179 0180 endInsertRows(); 0181 } 0182 0183 void CertificatesModel::importCertificate(const QUrl &path) 0184 { 0185 tl::expected<int, QString> maybeResult = importPrivate(path); 0186 0187 if (!maybeResult) { 0188 qWarning() << "Failed to import" << maybeResult.error(); 0189 Q_EMIT importError(maybeResult.error()); 0190 } 0191 } 0192 0193 tl::expected<int, QString> CertificatesModel::importPrivate(const QUrl &url) 0194 { 0195 if (url.isEmpty()) { 0196 return tl::make_unexpected(i18n("Empty file url")); 0197 } 0198 0199 if (!url.isValid()) { 0200 return tl::make_unexpected(i18n("File URL not valid: %1", url.toDisplayString())); 0201 } 0202 0203 QFile certFile(toLocalFile(url)); 0204 0205 bool ok = certFile.open(QFile::ReadOnly); 0206 0207 if (!ok) { 0208 return tl::make_unexpected(i18n("Could not open file: %1", toLocalFile(url))); 0209 } 0210 0211 const QByteArray data = certFile.readAll(); 0212 0213 std::optional<AnyCertificate> maybeCertificate = parseCertificate(data); 0214 if (maybeCertificate) { 0215 addCertificate(*maybeCertificate); 0216 return 1; 0217 } else { 0218 // let's see if this is a PDF containing barcodes instead 0219 KItinerary::ExtractorEngine engine; 0220 // user opened the file, so we can be reasonably sure they assume it contains 0221 // relevant content, so try expensive extraction methods too 0222 engine.setHints(KItinerary::ExtractorEngine::ExtractFullPageRasterImages); 0223 engine.setData(data, url.path()); 0224 engine.extract(); 0225 if (auto count = findRecursive(engine.rootDocumentNode())) { 0226 return count; 0227 } else { 0228 return tl::make_unexpected(i18n("No certificate found in %1", toLocalFile(url))); 0229 } 0230 } 0231 } 0232 0233 std::optional<AnyCertificate> CertificatesModel::parseCertificate(const QByteArray &data) const 0234 { 0235 const QVariant maybeCertificate = KHealthCertificateParser::parse(data); 0236 0237 if (maybeCertificate.userType() == qMetaTypeId<KVaccinationCertificate>()) { 0238 return maybeCertificate.value<KVaccinationCertificate>(); 0239 } 0240 0241 if (maybeCertificate.userType() == qMetaTypeId<KTestCertificate>()) { 0242 return maybeCertificate.value<KTestCertificate>(); 0243 } 0244 0245 if (maybeCertificate.userType() == qMetaTypeId<KRecoveryCertificate>()) { 0246 return maybeCertificate.value<KRecoveryCertificate>(); 0247 } 0248 0249 return {}; 0250 } 0251 0252 int CertificatesModel::findRecursive(const KItinerary::ExtractorDocumentNode &node) 0253 { 0254 // can possibly contain a barcode 0255 if (node.childNodes().size() == 1 0256 && (node.mimeType() == QLatin1String("internal/qimage") || node.mimeType() == QLatin1String("application/vnd.apple.pkpass"))) { 0257 const auto &child = node.childNodes()[0]; 0258 std::optional<AnyCertificate> cert; 0259 if (child.isA<QString>()) { 0260 cert = parseCertificate(child.content<QString>().toUtf8()); 0261 } else if (child.isA<QByteArray>()) { 0262 cert = parseCertificate(child.content<QByteArray>()); 0263 } 0264 0265 if (cert) { 0266 addCertificate(*cert); 0267 return 1; 0268 } 0269 } 0270 0271 // recurse 0272 int count = 0; 0273 for (const auto &child : node.childNodes()) { 0274 count += findRecursive(child); 0275 } 0276 return count; 0277 } 0278 0279 void CertificatesModel::importCertificateFromClipboard() 0280 { 0281 const auto md = QGuiApplication::clipboard()->mimeData(); 0282 bool result = false; 0283 if (md->hasText()) { 0284 result = importCertificateFromText(md->text()); 0285 } else if (md->hasFormat(QLatin1String("application/octet-stream"))) { 0286 result = importCertificateFromData(md->data(QLatin1String("application/octet-stream"))); 0287 } 0288 0289 if (!result) { 0290 Q_EMIT importError(i18n("No certificate in clipboard")); 0291 } 0292 } 0293 0294 bool CertificatesModel::importCertificateFromText(const QString &text) 0295 { 0296 return importCertificateFromData(text.toUtf8()); 0297 } 0298 0299 bool CertificatesModel::importCertificateFromData(const QByteArray &data) 0300 { 0301 auto maybeCert = parseCertificate(data); 0302 if (maybeCert) { 0303 addCertificate(*maybeCert); 0304 return true; 0305 } 0306 0307 return false; 0308 } 0309 0310 QString CertificatesModel::toLocalFile(const QUrl &url) 0311 { 0312 #ifdef Q_OS_ANDROID 0313 // toLocalFile makes content:/ URLs kaputt 0314 return url.toString(); 0315 #else 0316 return url.toLocalFile(); 0317 #endif 0318 } 0319 0320 bool CertificatesModel::removeRow(int row, const QModelIndex &parent) 0321 { 0322 return QAbstractListModel::removeRow(row, parent); 0323 } 0324 0325 bool CertificatesModel::removeRows(int row, int count, const QModelIndex &parent) 0326 { 0327 if (parent.isValid()) { 0328 return false; 0329 } 0330 0331 beginRemoveRows({}, row, row + count - 1); 0332 m_certificates.erase(m_certificates.begin() + row, m_certificates.begin() + row + count); 0333 if (!m_testMode) { 0334 m_generalConfig.writeEntry(QStringLiteral("certificates"), toStringList(m_certificates)); 0335 } 0336 endRemoveRows(); 0337 return true; 0338 }