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"