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 }