hafasmgatebackend.cpp 13.5 KB
Newer Older
1
/*
2
    SPDX-FileCopyrightText: 2018 Volker Krause <vkrause@kde.org>
3

4
    SPDX-License-Identifier: LGPL-2.0-or-later
5
6
7
*/

#include "hafasmgatebackend.h"
8
9
#include "hafasmgateparser.h"
#include "logging.h"
10
#include "cache.h"
11

12
13
14
#include <KPublicTransport/Journey>
#include <KPublicTransport/JourneyReply>
#include <KPublicTransport/JourneyRequest>
15
#include <KPublicTransport/Location>
16
17
#include <KPublicTransport/LocationReply>
#include <KPublicTransport/LocationRequest>
Volker Krause's avatar
Volker Krause committed
18
#include <KPublicTransport/Stopover>
19
#include <KPublicTransport/StopoverReply>
20
#include <KPublicTransport/StopoverRequest>
21

22
#include <QCryptographicHash>
23
24
25
26
27
#include <QDateTime>
#include <QDebug>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
28
#include <QMetaEnum>
29
30
31
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
32
#include <QUrlQuery>
33
#include <QVersionNumber>
34

35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
namespace KPublicTransport {

class HafasMgateRequestContext
{
public:
    QDateTime dateTime;
    int duration = 0;

    inline operator QVariant() const {
        return QVariant::fromValue(*this);
    }
};

}

Q_DECLARE_METATYPE(KPublicTransport::HafasMgateRequestContext)

52
53
using namespace KPublicTransport;

54
55
HafasMgateBackend::HafasMgateBackend() = default;
HafasMgateBackend::~HafasMgateBackend() = default;
56

57
58
void HafasMgateBackend::init()
{
59
    m_parser.setLocationIdentifierTypes(locationIdentifierType(), standardLocationIdentifierType());
60
    m_parser.setLineModeMap(std::move(m_lineModeMap));
61
    m_parser.setStandardLocationIdentfierCountries(std::move(m_uicCountryCodes));
62
63
}

64
AbstractBackend::Capabilities HafasMgateBackend::capabilities() const
65
{
66
67
    return (m_endpoint.startsWith(QLatin1String("https")) ? Secure : NoCapability)
        | CanQueryArrivals | CanQueryPreviousDeparture | CanQueryPreviousJourney | CanQueryNextJourney;
68
69
}

70
71
72
bool HafasMgateBackend::needsLocationQuery(const Location &loc, AbstractBackend::QueryType type) const
{
    Q_UNUSED(type);
73
    return !loc.hasCoordinate() && locationIdentifier(loc).isEmpty();
74
75
}

76
QJsonObject HafasMgateBackend::locationToJson(const Location &loc) const
77
{
78
79
80
81
82
83
84
85
86
87
88
89
90
91
    QJsonObject obj;

    const auto id = locationIdentifier(loc);
    if (!id.isEmpty()) {
        obj.insert(QStringLiteral("extId"), id);
        obj.insert(QStringLiteral("type"), QStringLiteral("S")); // 'S' == station
    }

    else if (loc.hasCoordinate()) {
        QJsonObject crd;
        crd.insert(QStringLiteral("y"), (int)(loc.latitude() * 1000000));
        crd.insert(QStringLiteral("x"), (int)(loc.longitude() * 1000000));
        obj.insert(QStringLiteral("crd"), crd);
        obj.insert(QStringLiteral("type"), QStringLiteral("C")); // 'C' == coordinate
92
93
    }

94
95
96
97
98
    return obj;
}

bool HafasMgateBackend::queryJourney(const JourneyRequest &request, JourneyReply *reply, QNetworkAccessManager *nam) const
{
99
100
101
102
    if ((request.modes() & JourneySection::PublicTransport) == 0) {
        return false;
    }

103
104
105
    QJsonObject tripSearch;
    {
        QJsonObject cfg;
106
        cfg.insert(QStringLiteral("polyEnc"), QLatin1String("GPA"));
107

108
109
110
111
112
        const auto depLoc = locationToJson(request.from());
        const auto arrLoc = locationToJson(request.to());
        if (depLoc.isEmpty() || arrLoc.isEmpty()) {
            return false;
        }
113
        QJsonArray depLocL;
114
        depLocL.push_back(depLoc);
115
        QJsonArray arrLocL;
116
        arrLocL.push_back(arrLoc);
117
118

        QJsonObject req;
119
120
121
122
123
        req.insert(QStringLiteral("arrLocL"), arrLocL);
        req.insert(QStringLiteral("depLocL"), depLocL);
        req.insert(QStringLiteral("extChgTime"), -1);
        req.insert(QStringLiteral("getEco"), false);
        req.insert(QStringLiteral("getIST"), false);
124
        req.insert(QStringLiteral("getPasslist"), request.includeIntermediateStops());
125
        req.insert(QStringLiteral("getPolyline"), request.includePaths());
126
127
        req.insert(QStringLiteral("getSimpleTrainComposition"), true);
        req.insert(QStringLiteral("getTrainComposition"), true);
128
        req.insert(QStringLiteral("numF"), request.maximumResults());
129

130
131
132
133
134
135
        QDateTime dt = request.dateTime();
        if (timeZone().isValid()) {
            dt = dt.toTimeZone(timeZone());
        }
        req.insert(QStringLiteral("outDate"), dt.date().toString(QStringLiteral("yyyyMMdd")));
        req.insert(QStringLiteral("outTime"), dt.time().toString(QStringLiteral("hhmmss")));
136
        req.insert(QStringLiteral("outFrwd"), request.dateTimeMode() == JourneyRequest::Departure);
137
        const auto ctxSrc = requestContextData(request).toString();
138
139
140
        if (!ctxSrc.isEmpty()) {
            req.insert(QStringLiteral("ctxScr"), ctxSrc);
        }
141
142
143
144

        tripSearch.insert(QStringLiteral("cfg"), cfg);
        tripSearch.insert(QStringLiteral("meth"), QLatin1String("TripSearch"));
        tripSearch.insert(QStringLiteral("req"), req);
145
146
    }

147
148
149
150
    QByteArray postData;
    const auto netRequest = makePostRequest(tripSearch, postData);
    logRequest(request, netRequest, postData);
    auto netReply = nam->post(netRequest, postData);
151
    QObject::connect(netReply, &QNetworkReply::finished, reply, [netReply, reply, this]() {
152
153
154
        const auto data = netReply->readAll();
        logReply(reply, netReply, data);

155
156
157
        switch (netReply->error()) {
            case QNetworkReply::NoError:
            {
158
                auto res = m_parser.parseJourneys(data);
159
                if (m_parser.error() == Reply::NoError) {
160
161
                    setNextRequestContext(reply, m_parser.m_nextJourneyContext);
                    setPreviousRequestContext(reply, m_parser.m_previousJourneyContext);
162
                    addResult(reply, this, std::move(res));
163
                } else {
164
                    addError(reply, m_parser.error(), m_parser.errorMessage());
165
166
167
168
                }
                break;
            }
            default:
169
                addError(reply, Reply::NetworkError, netReply->errorString());
170
171
172
173
174
175
                break;
        }
        netReply->deleteLater();
    });

    return true;
176
177
}

178
bool HafasMgateBackend::queryStopover(const StopoverRequest &request, StopoverReply *reply, QNetworkAccessManager *nam) const
179
{
180
181
    const auto stbLoc = locationToJson(request.stop());
    if (stbLoc.isEmpty()) {
182
183
        return false;
    }
184

185
    const auto ctx = requestContextData(request).value<HafasMgateRequestContext>();
186
187
188
189
190
    auto dt = ctx.dateTime.isValid() ? ctx.dateTime : request.dateTime();
    if (timeZone().isValid()) {
        dt = dt.toTimeZone(timeZone());
    }

191
192
193
    QJsonObject stationBoard;
    {
        QJsonObject req;
194
        req.insert(QStringLiteral("date"), dt.toString(QStringLiteral("yyyyMMdd")));
195
196
197
198
199
        if (ctx.duration > 0) {
            req.insert(QStringLiteral("dur"), QString::number(ctx.duration));
        } else {
            req.insert(QStringLiteral("maxJny"), request.maximumResults());
        }
200
201
        // stbFltrEquiv is no longer allowed above API version 1.20
        if (QVersionNumber::fromString(m_version) < QVersionNumber(1, 20)) {
202
203
            req.insert(QStringLiteral("stbFltrEquiv"), true);
        }
204

205
        req.insert(QStringLiteral("stbLoc"), stbLoc);
206
        req.insert(QStringLiteral("time"), dt.toString(QStringLiteral("hhmmss")));
207
        req.insert(QStringLiteral("type"), request.mode() == StopoverRequest::QueryDeparture ? QLatin1String("DEP") : QLatin1String("ARR"));
208

209
210
        stationBoard.insert(QStringLiteral("meth"), QLatin1String("StationBoard"));
        stationBoard.insert(QStringLiteral("req"), req);
211
212
    }

213
214
215
216
    QByteArray postData;
    const auto netRequest = makePostRequest(stationBoard, postData);
    logRequest(request, netRequest, postData);
    auto netReply = nam->post(netRequest, postData);
217
    QObject::connect(netReply, &QNetworkReply::finished, reply, [netReply, reply, dt, this]() {
218
219
220
        const auto data = netReply->readAll();
        logReply(reply, netReply, data);

221
222
223
        switch (netReply->error()) {
            case QNetworkReply::NoError:
            {
224
                auto result = m_parser.parseDepartures(data);
225
                if (m_parser.error() != Reply::NoError) {
226
                    addError(reply, m_parser.error(), m_parser.errorMessage());
227
                } else {
228
229
230
231
232
                    HafasMgateRequestContext prevCtx;
                    prevCtx.dateTime = dt.addSecs(-3600); // TODO: follow duration parameter in request once we have that
                    prevCtx.duration = 60;
                    setPreviousRequestContext(reply, prevCtx);

233
                    addResult(reply, this, std::move(result));
234
235
236
237
                }
                break;
            }
            default:
238
                addError(reply, Reply::NetworkError, netReply->errorString());
239
240
241
242
                break;
        }
        netReply->deleteLater();
    });
243
244

    return true;
245
246
}

247
bool HafasMgateBackend::queryLocation(const LocationRequest &req, LocationReply *reply, QNetworkAccessManager *nam) const
248
{
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
    if ((req.types() & Location::Stop) == 0) {
        return false;
    }

    QJsonObject methodObj;
    if (req.hasCoordinate()) {
        QJsonObject coord;
        coord.insert(QStringLiteral("x"), (int)(req.longitude() * 1000000));
        coord.insert(QStringLiteral("y"), (int)(req.latitude() * 1000000));
        QJsonObject ring;
        ring.insert(QStringLiteral("cCrd"), coord);
        ring.insert(QStringLiteral("maxDist"), std::max(1, req.maximumDistance()));

        QJsonObject reqObj;
        reqObj.insert(QStringLiteral("ring"), ring);
        // ### make this configurable in LocationRequest
        reqObj.insert(QStringLiteral("getStops"), true);
        reqObj.insert(QStringLiteral("getPOIs"), false);
        reqObj.insert(QStringLiteral("maxLoc"), std::max(1, req.maximumResults()));

        methodObj.insert(QStringLiteral("meth"), QLatin1String("LocGeoPos"));
        methodObj.insert(QStringLiteral("req"), reqObj);

    } else if (!req.name().isEmpty()) {
        QJsonObject loc;
        loc.insert(QStringLiteral("name"), req.name()); // + '?' for auto completion search?
        loc.insert(QStringLiteral("type"), QLatin1String("S")); // station: S, address: A, POI: P

        QJsonObject input;
        input.insert(QStringLiteral("field"), QLatin1String("S"));
        input.insert(QStringLiteral("loc"), loc);
        input.insert(QStringLiteral("maxLoc"), std::max(1, req.maximumResults()));

        QJsonObject reqObj;
        reqObj.insert(QStringLiteral("input"), input);

        methodObj.insert(QStringLiteral("meth"), QLatin1String("LocMatch"));
        methodObj.insert(QStringLiteral("req"), reqObj);

    } else {
289
290
291
        return false;
    }

292
293
294
295
296
    QByteArray postData;
    const auto netRequest = makePostRequest(methodObj, postData);
    logRequest(req, netRequest, postData);
    const auto netReply = nam->post(netRequest, postData);

297
    QObject::connect(netReply, &QNetworkReply::finished, reply, [netReply, reply, this]() {
298
        qDebug() << netReply->request().url();
299
300
301
        const auto data = netReply->readAll();
        logReply(reply, netReply, data);

302
303
304
        switch (netReply->error()) {
            case QNetworkReply::NoError:
            {
305
                auto res = m_parser.parseLocations(data);
Volker Krause's avatar
Volker Krause committed
306
                if (m_parser.error() == Reply::NoError) {
307
                    Cache::addLocationCacheEntry(backendId(), reply->request().cacheKey(), res, {});
Volker Krause's avatar
Volker Krause committed
308
309
                    addResult(reply, std::move(res));
                } else {
310
                    addError(reply, m_parser.error(), m_parser.errorMessage());
Volker Krause's avatar
Volker Krause committed
311
                }
312
313
314
                break;
            }
            default:
315
                addError(reply, Reply::NetworkError, netReply->errorString());
316
317
318
319
320
321
322
323
                break;
        }
        netReply->deleteLater();
    });

    return true;
}

324
QNetworkRequest HafasMgateBackend::makePostRequest(const QJsonObject &svcReq, QByteArray &postData) const
325
{
326
    QJsonObject top;
327
328
    top.insert(QStringLiteral("auth"), m_auth);
    top.insert(QStringLiteral("client"), m_client);
329
330
331
    if (!m_extParam.isEmpty()) {
        top.insert(QStringLiteral("ext"), m_extParam);
    }
332
    top.insert(QStringLiteral("formatted"), false);
333
    top.insert(QStringLiteral("lang"), preferredLanguage());
334
    top.insert(QStringLiteral("ver"), m_version);
335

336
337
338
339
    QJsonArray svcReqs;
    svcReqs.push_back(svcReq);
    top.insert(QStringLiteral("svcReqL"), svcReqs);

340
    postData = QJsonDocument(top).toJson(QJsonDocument::Compact);
341
    QUrl url(m_endpoint);
342
    QUrlQuery query;
343
344
    if (!m_micMacSalt.isEmpty()) {
        QCryptographicHash md5(QCryptographicHash::Md5);
345
        md5.addData(postData);
346
        const auto mic = md5.result().toHex();
347
        query.addQueryItem(QStringLiteral("mic"), QString::fromLatin1(mic));
348
349
350
351
352

        md5.reset();
        // yes, mic is added as hex-encoded string, and the salt is added as raw bytes
        md5.addData(mic);
        md5.addData(m_micMacSalt);
353
        query.addQueryItem(QStringLiteral("mac"), QString::fromLatin1(md5.result().toHex()));
354
    }
355
356
    if (!m_checksumSalt.isEmpty()) {
        QCryptographicHash md5(QCryptographicHash::Md5);
357
        md5.addData(postData);
358
        md5.addData(m_checksumSalt);
359
        query.addQueryItem(QStringLiteral("checksum"), QString::fromLatin1(md5.result().toHex()));
360
361
    }
    url.setQuery(query);
362
363

    auto netReq = QNetworkRequest(url);
364
    netReq.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/json"));
365
    applySslConfiguration(netReq);
366
    return netReq;
367
}
368

369
370
371
372
373
void HafasMgateBackend::setAuthObject(const QJsonObject& obj)
{
    m_auth = obj;
}

374
375
376
377
void HafasMgateBackend::setMicMacSalt(const QString &salt)
{
    m_micMacSalt = QByteArray::fromHex(salt.toUtf8());
}
378

379
380
381
382
void HafasMgateBackend::setChecksumSalt(const QString &salt)
{
    m_checksumSalt = QByteArray::fromHex(salt.toUtf8());
}