File indexing completed on 2024-05-12 04:42:35

0001 /*
0002     SPDX-FileCopyrightText: 2018 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "hafasmgatebackend.h"
0008 #include "logging.h"
0009 #include "cache.h"
0010 
0011 #include <KPublicTransport/Journey>
0012 #include <KPublicTransport/JourneyReply>
0013 #include <KPublicTransport/JourneyRequest>
0014 #include <KPublicTransport/Location>
0015 #include <KPublicTransport/LocationReply>
0016 #include <KPublicTransport/LocationRequest>
0017 #include <KPublicTransport/Stopover>
0018 #include <KPublicTransport/StopoverReply>
0019 #include <KPublicTransport/StopoverRequest>
0020 
0021 #include <QCryptographicHash>
0022 #include <QDateTime>
0023 #include <QDebug>
0024 #include <QJsonArray>
0025 #include <QJsonDocument>
0026 #include <QJsonObject>
0027 #include <QMetaEnum>
0028 #include <QNetworkAccessManager>
0029 #include <QNetworkReply>
0030 #include <QNetworkRequest>
0031 #include <QUrlQuery>
0032 #include <QVersionNumber>
0033 
0034 namespace KPublicTransport {
0035 
0036 class HafasMgateRequestContext
0037 {
0038 public:
0039     QDateTime dateTime;
0040     int duration = 0;
0041 
0042     inline operator QVariant() const {
0043         return QVariant::fromValue(*this);
0044     }
0045 };
0046 
0047 }
0048 
0049 Q_DECLARE_METATYPE(KPublicTransport::HafasMgateRequestContext)
0050 
0051 using namespace KPublicTransport;
0052 
0053 HafasMgateBackend::HafasMgateBackend() = default;
0054 HafasMgateBackend::~HafasMgateBackend() = default;
0055 
0056 void HafasMgateBackend::init()
0057 {
0058     m_parser.setLocationIdentifierTypes(locationIdentifierType(), standardLocationIdentifierType());
0059     m_parser.setLineModeMap(m_lineModeMap);
0060     m_parser.setStandardLocationIdentfierCountries(std::move(m_uicCountryCodes));
0061     m_parser.setProductNameMappings(std::move(m_productNameMappings));
0062 }
0063 
0064 AbstractBackend::Capabilities HafasMgateBackend::capabilities() const
0065 {
0066     return (m_endpoint.startsWith(QLatin1String("https")) ? Secure : NoCapability)
0067         | CanQueryArrivals | CanQueryPreviousDeparture | CanQueryPreviousJourney | CanQueryNextJourney;
0068 }
0069 
0070 bool HafasMgateBackend::needsLocationQuery(const Location &loc, AbstractBackend::QueryType type) const
0071 {
0072     Q_UNUSED(type);
0073     return !loc.hasCoordinate() && locationIdentifier(loc).isEmpty();
0074 }
0075 
0076 QJsonObject HafasMgateBackend::locationToJson(const Location &loc) const
0077 {
0078     QJsonObject obj;
0079 
0080     const auto id = locationIdentifier(loc);
0081     if (!id.isEmpty()) {
0082         obj.insert(QLatin1String("extId"), id);
0083         obj.insert(QLatin1String("type"), QLatin1String("S")); // 'S' == station
0084     }
0085 
0086     else if (loc.hasCoordinate()) {
0087         QJsonObject crd;
0088         crd.insert(QLatin1String("y"), (int)(loc.latitude() * 1000000));
0089         crd.insert(QLatin1String("x"), (int)(loc.longitude() * 1000000));
0090         obj.insert(QLatin1String("crd"), crd);
0091         obj.insert(QLatin1String("type"), QLatin1String("C")); // 'C' == coordinate
0092     }
0093 
0094     return obj;
0095 }
0096 
0097 bool HafasMgateBackend::queryJourney(const JourneyRequest &request, JourneyReply *reply, QNetworkAccessManager *nam) const
0098 {
0099     if ((request.modes() & JourneySection::PublicTransport) == 0) {
0100         return false;
0101     }
0102 
0103     QJsonObject tripSearch;
0104     {
0105         QJsonObject cfg;
0106         cfg.insert(QLatin1String("polyEnc"), QLatin1String("GPA"));
0107 
0108         const auto depLoc = locationToJson(request.from());
0109         const auto arrLoc = locationToJson(request.to());
0110         if (depLoc.isEmpty() || arrLoc.isEmpty()) {
0111             return false;
0112         }
0113         QJsonArray depLocL;
0114         depLocL.push_back(depLoc);
0115         QJsonArray arrLocL;
0116         arrLocL.push_back(arrLoc);
0117 
0118         QJsonObject req;
0119         req.insert(QLatin1String("arrLocL"), arrLocL);
0120         req.insert(QLatin1String("depLocL"), depLocL);
0121         req.insert(QLatin1String("extChgTime"), -1);
0122         req.insert(QLatin1String("getEco"), false);
0123         req.insert(QLatin1String("getIST"), false);
0124         req.insert(QLatin1String("getPasslist"), request.includeIntermediateStops());
0125         req.insert(QLatin1String("getPolyline"), request.includePaths());
0126         req.insert(QLatin1String("getSimpleTrainComposition"), true);
0127         req.insert(QLatin1String("getTrainComposition"), true);
0128         req.insert(QLatin1String("numF"), request.maximumResults());
0129 
0130         QDateTime dt = request.dateTime();
0131         if (timeZone().isValid()) {
0132             dt = dt.toTimeZone(timeZone());
0133         }
0134         req.insert(QLatin1String("outDate"), dt.date().toString(QStringLiteral("yyyyMMdd")));
0135         req.insert(QLatin1String("outTime"), dt.time().toString(QStringLiteral("hhmmss")));
0136         req.insert(QLatin1String("outFrwd"), request.dateTimeMode() == JourneyRequest::Departure);
0137         const auto ctxSrc = requestContextData(request).toString();
0138         if (!ctxSrc.isEmpty()) {
0139             req.insert(QLatin1String("ctxScr"), ctxSrc);
0140         }
0141 
0142         QJsonArray jnyFltrL;
0143         for (const auto &conGroup : m_conGroups) {
0144             const auto accessMatch = std::find(request.accessModes().begin(), request.accessModes().end(), conGroup.access) != request.accessModes().end();
0145             const auto egressMatch = std::find(request.egressModes().begin(), request.egressModes().end(), conGroup.egress) != request.egressModes().end();
0146             if (accessMatch && egressMatch) {
0147                 QJsonObject jnyFltr;
0148                 jnyFltr.insert(QLatin1String("mode"), QLatin1String("INC"));
0149                 jnyFltr.insert(QLatin1String("type"), QLatin1String("GROUP"));
0150                 jnyFltr.insert(QLatin1String("value"), conGroup.group);
0151                 jnyFltrL.push_back(jnyFltr);
0152             }
0153         }
0154         addLineModeJourneyFilter(request.lineModes(), jnyFltrL);
0155         if (!jnyFltrL.isEmpty()) {
0156             req.insert(QLatin1String("jnyFltrL"),  jnyFltrL);
0157         }
0158 
0159         tripSearch.insert(QLatin1String("cfg"), cfg);
0160         tripSearch.insert(QLatin1String("meth"), QLatin1String("TripSearch"));
0161         tripSearch.insert(QLatin1String("req"), req);
0162     }
0163 
0164     QByteArray postData;
0165     const auto netRequest = makePostRequest(tripSearch, postData);
0166     logRequest(request, netRequest, postData);
0167     auto netReply = nam->post(netRequest, postData);
0168     netReply->setParent(reply);
0169     QObject::connect(netReply, &QNetworkReply::finished, reply, [netReply, reply, this]() {
0170         const auto data = netReply->readAll();
0171         logReply(reply, netReply, data);
0172 
0173         switch (netReply->error()) {
0174             case QNetworkReply::NoError:
0175             {
0176                 auto res = m_parser.parseJourneys(data);
0177                 if (m_parser.error() == Reply::NoError) {
0178                     setNextRequestContext(reply, m_parser.m_nextJourneyContext);
0179                     setPreviousRequestContext(reply, m_parser.m_previousJourneyContext);
0180                     addResult(reply, this, std::move(res));
0181                 } else {
0182                     addError(reply, m_parser.error(), m_parser.errorMessage());
0183                 }
0184                 break;
0185             }
0186             default:
0187                 addError(reply, Reply::NetworkError, netReply->errorString());
0188                 break;
0189         }
0190         netReply->deleteLater();
0191     });
0192 
0193     return true;
0194 }
0195 
0196 bool HafasMgateBackend::queryStopover(const StopoverRequest &request, StopoverReply *reply, QNetworkAccessManager *nam) const
0197 {
0198     const auto stbLoc = locationToJson(request.stop());
0199     if (stbLoc.isEmpty()) {
0200         return false;
0201     }
0202 
0203     const auto ctx = requestContextData(request).value<HafasMgateRequestContext>();
0204     auto dt = ctx.dateTime.isValid() ? ctx.dateTime : request.dateTime();
0205     if (timeZone().isValid()) {
0206         dt = dt.toTimeZone(timeZone());
0207     }
0208 
0209     QJsonObject stationBoard;
0210     {
0211         QJsonObject req;
0212         req.insert(QLatin1String("date"), dt.toString(QStringLiteral("yyyyMMdd")));
0213         if (ctx.duration > 0) {
0214             req.insert(QLatin1String("dur"), QString::number(ctx.duration));
0215         } else {
0216             req.insert(QLatin1String("maxJny"), request.maximumResults());
0217         }
0218         // stbFltrEquiv is no longer allowed above API version 1.20
0219         if (QVersionNumber::fromString(m_version) < QVersionNumber(1, 20)) {
0220             req.insert(QLatin1String("stbFltrEquiv"), true);
0221         }
0222 
0223         req.insert(QLatin1String("stbLoc"), stbLoc);
0224         req.insert(QLatin1String("time"), dt.toString(QStringLiteral("hhmmss")));
0225         req.insert(QLatin1String("type"), request.mode() == StopoverRequest::QueryDeparture ? QLatin1String("DEP") : QLatin1String("ARR"));
0226 
0227         QJsonArray jnyFltrL;
0228         addLineModeJourneyFilter(request.lineModes(), jnyFltrL);
0229         if (!jnyFltrL.isEmpty()) {
0230             req.insert(QLatin1String("jnyFltrL"),  jnyFltrL);
0231         }
0232 
0233         stationBoard.insert(QLatin1String("meth"), QLatin1String("StationBoard"));
0234         stationBoard.insert(QLatin1String("req"), req);
0235     }
0236 
0237     QByteArray postData;
0238     const auto netRequest = makePostRequest(stationBoard, postData);
0239     logRequest(request, netRequest, postData);
0240     auto netReply = nam->post(netRequest, postData);
0241     netReply->setParent(reply);
0242     QObject::connect(netReply, &QNetworkReply::finished, reply, [netReply, reply, dt, this]() {
0243         const auto data = netReply->readAll();
0244         logReply(reply, netReply, data);
0245 
0246         switch (netReply->error()) {
0247             case QNetworkReply::NoError:
0248             {
0249                 auto result = m_parser.parseDepartures(data);
0250                 if (m_parser.error() != Reply::NoError) {
0251                     addError(reply, m_parser.error(), m_parser.errorMessage());
0252                 } else {
0253                     HafasMgateRequestContext prevCtx;
0254                     prevCtx.dateTime = dt.addSecs(-3600); // TODO: follow duration parameter in request once we have that
0255                     prevCtx.duration = 60;
0256                     setPreviousRequestContext(reply, prevCtx);
0257 
0258                     addResult(reply, this, std::move(result));
0259                 }
0260                 break;
0261             }
0262             default:
0263                 addError(reply, Reply::NetworkError, netReply->errorString());
0264                 break;
0265         }
0266         netReply->deleteLater();
0267     });
0268 
0269     return true;
0270 }
0271 
0272 bool HafasMgateBackend::queryLocation(const LocationRequest &req, LocationReply *reply, QNetworkAccessManager *nam) const
0273 {
0274     if ((req.types() & Location::Stop) == 0) {
0275         return false;
0276     }
0277 
0278     QJsonObject methodObj;
0279     if (req.hasCoordinate()) {
0280         QJsonObject coord;
0281         coord.insert(QLatin1String("x"), (int)(req.longitude() * 1000000));
0282         coord.insert(QLatin1String("y"), (int)(req.latitude() * 1000000));
0283         QJsonObject ring;
0284         ring.insert(QLatin1String("cCrd"), coord);
0285         ring.insert(QLatin1String("maxDist"), std::max(1, req.maximumDistance()));
0286 
0287         QJsonObject reqObj;
0288         reqObj.insert(QLatin1String("ring"), ring);
0289         // ### make this configurable in LocationRequest
0290         reqObj.insert(QLatin1String("getStops"), true);
0291         reqObj.insert(QLatin1String("getPOIs"), false);
0292         reqObj.insert(QLatin1String("maxLoc"), std::max(1, req.maximumResults()));
0293 
0294         methodObj.insert(QLatin1String("meth"), QLatin1String("LocGeoPos"));
0295         methodObj.insert(QLatin1String("req"), reqObj);
0296 
0297     } else if (!req.name().isEmpty()) {
0298         QJsonObject loc;
0299         loc.insert(QLatin1String("name"), req.name()); // + '?' for auto completion search?
0300         loc.insert(QLatin1String("type"), QLatin1String("S")); // station: S, address: A, POI: P
0301 
0302         QJsonObject input;
0303         input.insert(QLatin1String("field"), QLatin1String("S"));
0304         input.insert(QLatin1String("loc"), loc);
0305         input.insert(QLatin1String("maxLoc"), std::max(1, req.maximumResults()));
0306 
0307         QJsonObject reqObj;
0308         reqObj.insert(QLatin1String("input"), input);
0309 
0310         methodObj.insert(QLatin1String("meth"), QLatin1String("LocMatch"));
0311         methodObj.insert(QLatin1String("req"), reqObj);
0312 
0313     } else {
0314         return false;
0315     }
0316 
0317     QByteArray postData;
0318     const auto netRequest = makePostRequest(methodObj, postData);
0319     logRequest(req, netRequest, postData);
0320     const auto netReply = nam->post(netRequest, postData);
0321     netReply->setParent(reply);
0322 
0323     QObject::connect(netReply, &QNetworkReply::finished, reply, [netReply, reply, this]() {
0324         qDebug() << netReply->request().url();
0325         const auto data = netReply->readAll();
0326         logReply(reply, netReply, data);
0327 
0328         switch (netReply->error()) {
0329             case QNetworkReply::NoError:
0330             {
0331                 auto res = m_parser.parseLocations(data);
0332                 if (m_parser.error() == Reply::NoError) {
0333                     Cache::addLocationCacheEntry(backendId(), reply->request().cacheKey(), res, {});
0334                     addResult(reply, std::move(res));
0335                 } else {
0336                     addError(reply, m_parser.error(), m_parser.errorMessage());
0337                 }
0338                 break;
0339             }
0340             default:
0341                 addError(reply, Reply::NetworkError, netReply->errorString());
0342                 break;
0343         }
0344         netReply->deleteLater();
0345     });
0346 
0347     return true;
0348 }
0349 
0350 QNetworkRequest HafasMgateBackend::makePostRequest(const QJsonObject &svcReq, QByteArray &postData) const
0351 {
0352     QJsonObject top;
0353     top.insert(QLatin1String("auth"), m_auth);
0354     top.insert(QLatin1String("client"), m_client);
0355     if (!m_extParam.isEmpty()) {
0356         top.insert(QLatin1String("ext"), m_extParam);
0357     }
0358     top.insert(QLatin1String("formatted"), false);
0359     top.insert(QLatin1String("lang"), preferredLanguage());
0360     top.insert(QLatin1String("ver"), m_version);
0361 
0362     QJsonArray svcReqs;
0363     svcReqs.push_back(svcReq);
0364     top.insert(QLatin1String("svcReqL"), svcReqs);
0365 
0366     postData = QJsonDocument(top).toJson(QJsonDocument::Compact);
0367     QUrl url(m_endpoint);
0368     QUrlQuery query;
0369     if (!m_micMacSalt.isEmpty()) {
0370         QCryptographicHash md5(QCryptographicHash::Md5);
0371         md5.addData(postData);
0372         const auto mic = md5.result().toHex();
0373         query.addQueryItem(QStringLiteral("mic"), QString::fromLatin1(mic));
0374 
0375         md5.reset();
0376         // yes, mic is added as hex-encoded string, and the salt is added as raw bytes
0377         md5.addData(mic);
0378         md5.addData(m_micMacSalt);
0379         query.addQueryItem(QStringLiteral("mac"), QString::fromLatin1(md5.result().toHex()));
0380     }
0381     if (!m_checksumSalt.isEmpty()) {
0382         QCryptographicHash md5(QCryptographicHash::Md5);
0383         md5.addData(postData);
0384         md5.addData(m_checksumSalt);
0385         query.addQueryItem(QStringLiteral("checksum"), QString::fromLatin1(md5.result().toHex()));
0386     }
0387     url.setQuery(query);
0388 
0389     auto netReq = QNetworkRequest(url);
0390     netReq.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/json"));
0391     applySslConfiguration(netReq);
0392     return netReq;
0393 }
0394 
0395 void HafasMgateBackend::setMicMacSalt(const QString &salt)
0396 {
0397     m_micMacSalt = QByteArray::fromHex(salt.toUtf8());
0398 }
0399 
0400 void HafasMgateBackend::setChecksumSalt(const QString &salt)
0401 {
0402     m_checksumSalt = QByteArray::fromHex(salt.toUtf8());
0403 }
0404 
0405 void HafasMgateBackend::setConGroups(const QJsonArray &conGroups)
0406 {
0407     m_conGroups.reserve(conGroups.size());
0408     for (const auto &conGroupVal : conGroups) {
0409         const auto conGroupObj = conGroupVal.toObject();
0410         ConGroup cg;
0411         cg.access = IndividualTransport::fromJson(conGroupObj.value(QLatin1String("access")).toObject());
0412         cg.egress = IndividualTransport::fromJson(conGroupObj.value(QLatin1String("egress")).toObject());
0413         cg.group = conGroupObj.value(QLatin1String("conGroup")).toString();
0414         m_conGroups.push_back(std::move(cg));
0415     }
0416 }
0417 
0418 static QStringList parseProductNameMappingFieldNames(const QJsonValue &val)
0419 {
0420     if (val.isString()) {
0421         return QStringList({val.toString()});
0422     }
0423     if (val.isArray()) {
0424         const auto a = val.toArray();
0425         QStringList l;
0426         l.reserve(a.size());
0427         std::transform(a.begin(), a.end(), std::back_inserter(l), [](const auto &v) { return v.toString(); });
0428         return l;
0429     }
0430     return {};
0431 }
0432 
0433 void HafasMgateBackend::setProductNameMappings(const QJsonArray &productNameMappings)
0434 {
0435     m_productNameMappings.reserve(productNameMappings.size());
0436     for (const auto &mV : productNameMappings) {
0437         const auto mObj = mV.toObject();
0438         HafasMgateProductNameMapping m;
0439         m.cls = mObj.value(QLatin1String("cls")).toInt(-1);
0440         m.lineName = parseProductNameMappingFieldNames(mObj.value(QLatin1String("lineName")));
0441         m.routeName = parseProductNameMappingFieldNames(mObj.value(QLatin1String("routeName")));
0442         m_productNameMappings.push_back(std::move(m));
0443     }
0444 }
0445 
0446 void HafasMgateBackend::addLineModeJourneyFilter(const std::vector<Line::Mode> &lineModes, QJsonArray &jnyFltrL) const
0447 {
0448     if (lineModes.empty()) {
0449         return;
0450     }
0451 
0452     int productBitmask = 0;
0453     for (const auto mode : lineModes) {
0454         for (const auto& [key, value] : m_lineModeMap) {
0455             if (value == mode)  {
0456                 productBitmask |= key;
0457             }
0458         }
0459     }
0460 
0461     if (productBitmask != 0) {
0462         jnyFltrL.push_back(QJsonObject({
0463             {QLatin1String("type"), QLatin1String("PROD")},
0464             {QLatin1String("mode"), QLatin1String("INC")},
0465             {QLatin1String("value"), QString::number(productBitmask)}
0466         }));
0467     }
0468 }