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 }