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