1 /*
2     SPDX-FileCopyrightText: 2018 Volker Krause <vkrause@kde.org>
3 
4     SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 #include "pkpassmanager.h"
8 #include "logging.h"
9 
10 #include <KItinerary/Reservation>
11 #include <KPkPass/Pass>
12 
13 #include <QDateTime>
14 #include <QDebug>
15 #include <QDir>
16 #include <QDirIterator>
17 #include <QFile>
18 #include <QNetworkAccessManager>
19 #include <QNetworkReply>
20 #include <QStandardPaths>
21 #include <QTemporaryFile>
22 #include <QUrl>
23 #include <QVector>
24 
25 using namespace KItinerary;
26 
PkPassManager(QObject * parent)27 PkPassManager::PkPassManager(QObject* parent)
28     : QObject(parent)
29     , m_nam(new QNetworkAccessManager(this))
30 {
31 }
32 
33 PkPassManager::~PkPassManager() = default;
34 
passes() const35 QVector<QString> PkPassManager::passes() const
36 {
37     const QString basePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/passes");
38     QDir::root().mkpath(basePath);
39 
40     QVector<QString> passIds;
41     for (QDirIterator topIt(basePath, QDir::NoDotAndDotDot | QDir::Dirs); topIt.hasNext();) {
42         for (QDirIterator subIt(topIt.next(), QDir::Files); subIt.hasNext();) {
43             QFileInfo fi(subIt.next());
44             passIds.push_back(fi.dir().dirName() + QLatin1Char('/') + fi.baseName());
45         }
46     }
47 
48     return passIds;
49 }
50 
hasPass(const QString & passId) const51 bool PkPassManager::hasPass(const QString &passId) const
52 {
53     if (m_passes.contains(passId)) {
54         return true;
55     }
56 
57     const QString passPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QLatin1String("/passes/") + passId + QLatin1String(".pkpass");
58     return QFile::exists(passPath);
59 }
60 
pass(const QString & passId)61 KPkPass::Pass* PkPassManager::pass(const QString& passId)
62 {
63     const auto it = m_passes.constFind(passId);
64     if (it != m_passes.constEnd() && it.value()) {
65         return it.value();
66     }
67 
68     const QString passPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QLatin1String("/passes/") + passId + QLatin1String(".pkpass");
69     if (!QFile::exists(passPath)) {
70         return nullptr;
71     }
72 
73     auto file = KPkPass::Pass::fromFile(passPath, this);
74     // TODO error handling
75     m_passes.insert(passId, file);
76     return file;
77 }
78 
passObject(const QString & passId)79 QObject* PkPassManager::passObject(const QString& passId)
80 {
81     return pass(passId);
82 }
83 
passId(const QVariant & reservation) const84 QString PkPassManager::passId(const QVariant &reservation) const
85 {
86     if (!JsonLd::canConvert<Reservation>(reservation)) {
87         return {};
88     }
89     const auto res = JsonLd::convert<Reservation>(reservation);
90     const auto passTypeId = res.pkpassPassTypeIdentifier();
91     const auto serialNum = res.pkpassSerialNumber();
92     if (passTypeId.isEmpty() || serialNum.isEmpty()) {
93         return {};
94     }
95     return passTypeId + QLatin1Char('/') + QString::fromUtf8(serialNum.toUtf8().toBase64(QByteArray::Base64UrlEncoding));
96 }
97 
importPass(const QUrl & url)98 QString PkPassManager::importPass(const QUrl& url)
99 {
100     return doImportPass(url, {}, Copy);
101 }
102 
importPassFromTempFile(const QUrl & tmpFile)103 void PkPassManager::importPassFromTempFile(const QUrl& tmpFile)
104 {
105     doImportPass(tmpFile, {}, Move);
106 }
107 
importPassFromData(const QByteArray & data)108 QString PkPassManager::importPassFromData(const QByteArray &data)
109 {
110     return doImportPass({}, data, Data);
111 }
112 
doImportPass(const QUrl & url,const QByteArray & data,PkPassManager::ImportMode mode)113 QString PkPassManager::doImportPass(const QUrl& url, const QByteArray &data, PkPassManager::ImportMode mode)
114 {
115     qCDebug(Log) << url << mode;
116     if (url.isEmpty() && data.isEmpty()) {
117         return {};
118     }
119 
120     const QString basePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/passes");
121     const auto fileName = url.isLocalFile() ? url.toLocalFile() : url.toString();
122     QDir::root().mkpath(basePath);
123 
124     std::unique_ptr<KPkPass::Pass> newPass;
125     if (!url.isEmpty()) {
126         newPass.reset(KPkPass::Pass::fromFile(fileName));
127     } else {
128         newPass.reset(KPkPass::Pass::fromData(data));
129     }
130 
131     if (!newPass) {
132         qCDebug(Log) << "Failed to load pkpass file" << url;
133         return {};
134     }
135     if (newPass->passTypeIdentifier().isEmpty() || newPass->serialNumber().isEmpty()) {
136         qCDebug(Log) << "PkPass file has no type identifier or serial number" << url;
137         return {};
138     }
139 
140     QDir dir(basePath);
141     dir.mkdir(newPass->passTypeIdentifier());
142     dir.cd(newPass->passTypeIdentifier());
143 
144     // serialNumber() can contain percent-encoding or slashes,
145     // ie stuff we don't want to have in file names
146     const auto serNum = QString::fromUtf8(newPass->serialNumber().toUtf8().toBase64(QByteArray::Base64UrlEncoding));
147     const QString passId = dir.dirName() + QLatin1Char('/') + serNum;
148 
149     auto oldPass = pass(passId);
150     if (oldPass) {
151         QFile::remove(dir.absoluteFilePath(serNum + QLatin1String(".pkpass")));
152         m_passes.remove(passId);
153     }
154 
155     switch (mode) {
156         case Move:
157             QFile::rename(fileName, dir.absoluteFilePath(serNum + QLatin1String(".pkpass")));
158             break;
159         case Copy:
160             QFile::copy(fileName, dir.absoluteFilePath(serNum + QLatin1String(".pkpass")));
161             break;
162         case Data:
163         {
164             QFile f(dir.absoluteFilePath(serNum + QLatin1String(".pkpass")));
165             if (!f.open(QFile::WriteOnly)) {
166                 qCWarning(Log) << "Failed to open file" << f.fileName() << f.errorString();
167                 break;
168             }
169             f.write(data);
170         }
171     }
172 
173     if (oldPass) {
174         // check for changes and generate change message
175         QStringList changes;
176         for (const auto &f : newPass->fields()) {
177             const auto prevValue = oldPass->field(f.key()).value();
178             const auto curValue = f.value();
179             if (curValue != prevValue) {
180                 changes.push_back(f.changeMessage());
181             }
182         }
183         Q_EMIT passUpdated(passId, changes);
184         oldPass->deleteLater();
185     } else {
186         Q_EMIT passAdded(passId);
187     }
188 
189     return passId;
190 }
191 
removePass(const QString & passId)192 void PkPassManager::removePass(const QString& passId)
193 {
194     const QString basePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/passes/");
195     QFile::remove(basePath + QLatin1Char('/') + passId + QLatin1String(".pkpass"));
196     Q_EMIT passRemoved(passId);
197     delete m_passes.take(passId);
198 }
199 
updatePass(const QString & passId)200 void PkPassManager::updatePass(const QString& passId)
201 {
202     auto p = pass(passId);
203     if (!p || p->webServiceUrl().isEmpty() || p->authenticationToken().isEmpty())
204         return;
205     if (relevantDate(p) < QDateTime::currentDateTimeUtc()) // TODO check expiration date and voided property
206         return;
207 
208     QNetworkRequest req(p->passUpdateUrl());
209     req.setRawHeader("Authorization", "ApplePass " + p->authenticationToken().toUtf8());
210     req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
211     auto reply = m_nam->get(req);
212     connect(reply, &QNetworkReply::finished, this, [this, reply]() {
213         if (reply->error() != QNetworkReply::NoError) {
214             qCWarning(Log) << "Failed to download pass:" << reply->errorString();
215             return;
216         }
217 
218         QTemporaryFile tmp;
219         tmp.open();
220         tmp.write(reply->readAll());
221         tmp.close();
222         importPassFromTempFile(QUrl::fromLocalFile(tmp.fileName()));
223     });
224 }
225 
updateTime(const QString & passId) const226 QDateTime PkPassManager::updateTime(const QString &passId) const
227 {
228     const QString passPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QLatin1String("/passes/") + passId + QLatin1String(".pkpass");
229     QFileInfo fi(passPath);
230     return fi.lastModified();
231 }
232 
relevantDate(KPkPass::Pass * pass)233 QDateTime PkPassManager::relevantDate(KPkPass::Pass *pass)
234 {
235     const auto dt = pass->relevantDate();
236     if (dt.isValid())
237         return dt;
238     return pass->expirationDate();
239 }
240 
rawData(const QString & passId) const241 QByteArray PkPassManager::rawData(const QString &passId) const
242 {
243     const QString passPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QLatin1String("/passes/") + passId + QLatin1String(".pkpass");
244     QFile f(passPath);
245     if (!f.open(QFile::ReadOnly)) {
246         qCWarning(Log) << "Failed to open pass file for pass" << passId;
247         return {};
248     }
249     return f.readAll();
250 }
251