File indexing completed on 2024-06-02 05:18:46

0001 /*
0002     SPDX-FileCopyrightText: 2018 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "pkpassmanager.h"
0008 #include "genericpkpass.h"
0009 #include "logging.h"
0010 
0011 #include <KItinerary/Reservation>
0012 #include <KPkPass/Pass>
0013 
0014 #include <QDateTime>
0015 #include <QDebug>
0016 #include <QDir>
0017 #include <QDirIterator>
0018 #include <QFile>
0019 #include <QNetworkAccessManager>
0020 #include <QNetworkReply>
0021 #include <QStandardPaths>
0022 #include <QTemporaryFile>
0023 #include <QUrl>
0024 #include <QVector>
0025 
0026 using namespace KItinerary;
0027 
0028 PkPassManager::PkPassManager(QObject* parent)
0029     : QObject(parent)
0030 {
0031 }
0032 
0033 PkPassManager::~PkPassManager() = default;
0034 
0035 void PkPassManager::setNetworkAccessManagerFactory(const std::function<QNetworkAccessManager*()> &namFactory)
0036 {
0037     m_namFactory = namFactory;
0038 }
0039 
0040 QVector<QString> PkPassManager::passes() const
0041 {
0042     const QString basePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/passes");
0043     QDir::root().mkpath(basePath);
0044 
0045     QVector<QString> passIds;
0046     for (QDirIterator topIt(basePath, QDir::NoDotAndDotDot | QDir::Dirs); topIt.hasNext();) {
0047         for (QDirIterator subIt(topIt.next(), QDir::Files); subIt.hasNext();) {
0048             QFileInfo fi(subIt.next());
0049             passIds.push_back(fi.dir().dirName() + QLatin1Char('/') + fi.baseName());
0050         }
0051     }
0052 
0053     return passIds;
0054 }
0055 
0056 bool PkPassManager::hasPass(const QString &passId) const
0057 {
0058     if (m_passes.contains(passId)) {
0059         return true;
0060     }
0061 
0062     const QString passPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QLatin1StringView("/passes/") + passId + QLatin1StringView(".pkpass");
0063     return QFile::exists(passPath);
0064 }
0065 
0066 KPkPass::Pass* PkPassManager::pass(const QString& passId)
0067 {
0068     const auto it = m_passes.constFind(passId);
0069     if (it != m_passes.constEnd() && it.value()) {
0070         return it.value();
0071     }
0072 
0073     const QString passPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QLatin1StringView("/passes/") + passId + QLatin1StringView(".pkpass");
0074     if (!QFile::exists(passPath)) {
0075         return nullptr;
0076     }
0077 
0078     auto file = KPkPass::Pass::fromFile(passPath, this);
0079     // TODO error handling
0080     m_passes.insert(passId, file);
0081     return file;
0082 }
0083 
0084 QString PkPassManager::passId(const QVariant &reservation)
0085 {
0086     QString passTypeId, serialNum;
0087 
0088     if (JsonLd::canConvert<Reservation>(reservation)) {
0089         const auto res = JsonLd::convert<Reservation>(reservation);
0090         passTypeId = res.pkpassPassTypeIdentifier();
0091         serialNum = res.pkpassSerialNumber();
0092     } else if (JsonLd::isA<GenericPkPass>(reservation)) {
0093         const auto p = JsonLd::convert<GenericPkPass>(reservation);
0094         passTypeId = p.pkpassPassTypeIdentifier();
0095         serialNum = p.pkpassSerialNumber();
0096     }
0097 
0098     if (passTypeId.isEmpty() || serialNum.isEmpty()) {
0099         return {};
0100     }
0101     return passTypeId + QLatin1Char('/') + QString::fromUtf8(serialNum.toUtf8().toBase64(QByteArray::Base64UrlEncoding));
0102 }
0103 
0104 QString PkPassManager::importPass(const QUrl& url)
0105 {
0106     return doImportPass(url, {}, Copy);
0107 }
0108 
0109 void PkPassManager::importPassFromTempFile(const QUrl& tmpFile)
0110 {
0111     doImportPass(tmpFile, {}, Move);
0112 }
0113 
0114 QString PkPassManager::importPassFromData(const QByteArray &data)
0115 {
0116     return doImportPass({}, data, Data);
0117 }
0118 
0119 QString PkPassManager::doImportPass(const QUrl& url, const QByteArray &data, PkPassManager::ImportMode mode)
0120 {
0121     qCDebug(Log) << url << mode;
0122     if (url.isEmpty() && data.isEmpty()) {
0123         return {};
0124     }
0125 
0126     const QString basePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/passes");
0127     const auto fileName = url.isLocalFile() ? url.toLocalFile() : url.toString();
0128     QDir::root().mkpath(basePath);
0129 
0130     std::unique_ptr<KPkPass::Pass> newPass;
0131     if (!url.isEmpty()) {
0132         newPass.reset(KPkPass::Pass::fromFile(fileName));
0133     } else {
0134         newPass.reset(KPkPass::Pass::fromData(data));
0135     }
0136 
0137     if (!newPass) {
0138         qCDebug(Log) << "Failed to load pkpass file" << url;
0139         return {};
0140     }
0141     if (newPass->passTypeIdentifier().isEmpty() || newPass->serialNumber().isEmpty()) {
0142         qCDebug(Log) << "PkPass file has no type identifier or serial number" << url;
0143         return {};
0144     }
0145 
0146     QDir dir(basePath);
0147     dir.mkdir(newPass->passTypeIdentifier());
0148     dir.cd(newPass->passTypeIdentifier());
0149 
0150     // serialNumber() can contain percent-encoding or slashes,
0151     // ie stuff we don't want to have in file names
0152     const auto serNum = QString::fromUtf8(newPass->serialNumber().toUtf8().toBase64(QByteArray::Base64UrlEncoding));
0153     const QString passId = dir.dirName() + QLatin1Char('/') + serNum;
0154 
0155     auto oldPass = pass(passId);
0156     if (oldPass) {
0157         QFile::remove(dir.absoluteFilePath(serNum + QLatin1StringView(".pkpass")));
0158         m_passes.remove(passId);
0159     }
0160 
0161     switch (mode) {
0162         case Move:
0163             QFile::rename(fileName, dir.absoluteFilePath(serNum + QLatin1StringView(".pkpass")));
0164             break;
0165         case Copy:
0166             QFile::copy(fileName, dir.absoluteFilePath(serNum + QLatin1StringView(".pkpass")));
0167             break;
0168         case Data:
0169         {
0170             QFile f(dir.absoluteFilePath(serNum + QLatin1StringView(".pkpass")));
0171             if (!f.open(QFile::WriteOnly)) {
0172                 qCWarning(Log) << "Failed to open file" << f.fileName() << f.errorString();
0173                 break;
0174             }
0175             f.write(data);
0176         }
0177     }
0178 
0179     if (oldPass) {
0180         // check for changes and generate change message
0181         QStringList changes;
0182         for (const auto &f : newPass->fields()) {
0183             const auto prevValue = oldPass->field(f.key()).value();
0184             const auto curValue = f.value();
0185             if (curValue != prevValue) {
0186                 changes.push_back(f.changeMessage());
0187             }
0188         }
0189         Q_EMIT passUpdated(passId, changes);
0190         oldPass->deleteLater();
0191     } else {
0192         Q_EMIT passAdded(passId);
0193     }
0194 
0195     return passId;
0196 }
0197 
0198 void PkPassManager::removePass(const QString& passId)
0199 {
0200     const QString basePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/passes/");
0201     QFile::remove(basePath + QLatin1Char('/') + passId + QLatin1StringView(".pkpass"));
0202     Q_EMIT passRemoved(passId);
0203     delete m_passes.take(passId);
0204 }
0205 
0206 void PkPassManager::updatePass(const QString& passId)
0207 {
0208     auto p = pass(passId);
0209     if (!canUpdate(p)) {
0210         return;
0211     }
0212 
0213     QNetworkRequest req(p->passUpdateUrl());
0214     req.setRawHeader("Authorization", "ApplePass " + p->authenticationToken().toUtf8());
0215     req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
0216     qDebug() << req.url();
0217     auto reply = m_namFactory()->get(req);
0218     connect(reply, &QNetworkReply::finished, this, [this, reply]() {
0219         reply->deleteLater();
0220         qDebug() << reply->errorString();
0221         if (reply->error() != QNetworkReply::NoError) {
0222             qCWarning(Log) << "Failed to download pass:" << reply->errorString();
0223             return;
0224         }
0225 
0226         QTemporaryFile tmp;
0227         tmp.open();
0228         tmp.write(reply->readAll());
0229         tmp.close();
0230         importPassFromTempFile(QUrl::fromLocalFile(tmp.fileName()));
0231     });
0232 }
0233 
0234 QDateTime PkPassManager::updateTime(const QString &passId) const
0235 {
0236     const QString passPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QLatin1StringView("/passes/") + passId + QLatin1StringView(".pkpass");
0237     QFileInfo fi(passPath);
0238     return fi.lastModified();
0239 }
0240 
0241 QDateTime PkPassManager::relevantDate(KPkPass::Pass *pass)
0242 {
0243     const auto dt = pass->relevantDate();
0244     if (dt.isValid())
0245         return dt;
0246     return pass->expirationDate();
0247 }
0248 
0249 bool PkPassManager::canUpdate(KPkPass::Pass *pass)
0250 {
0251     return pass && pass->webServiceUrl().isValid() && !pass->authenticationToken().isEmpty();
0252 }
0253 
0254 QByteArray PkPassManager::rawData(const QString &passId) const
0255 {
0256     const QString passPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QLatin1StringView("/passes/") + passId + QLatin1StringView(".pkpass");
0257     QFile f(passPath);
0258     if (!f.open(QFile::ReadOnly)) {
0259         qCWarning(Log) << "Failed to open pass file for pass" << passId;
0260         return {};
0261     }
0262     return f.readAll();
0263 }
0264 
0265 #include "moc_pkpassmanager.cpp"