diff --git a/src/networking/internalnetworkaccessmanager.cpp b/src/networking/internalnetworkaccessmanager.cpp index c2f12af4..eb41ff2b 100644 --- a/src/networking/internalnetworkaccessmanager.cpp +++ b/src/networking/internalnetworkaccessmanager.cpp @@ -1,233 +1,244 @@ /*************************************************************************** * Copyright (C) 2004-2020 by Thomas Fischer * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, see . * ***************************************************************************/ #include "internalnetworkaccessmanager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#if QT_VERSION >= 0x050a00 #include +#endif // QT_VERSION #ifdef HAVE_KF5 #include #endif // HAVE_KF5 #include "logging_networking.h" +#if QT_VERSION >= 0x050a00 +#define randomGeneratorGlobalBounded(min,max) QRandomGenerator::global()->bounded((min),(max)) +#else // QT_VERSION +#define randomGeneratorGlobalBounded(min,max) ((min)+(qrand()%((max)-(min)+1))) +#endif // QT_VERSION + /** * @author Thomas Fischer */ class InternalNetworkAccessManager::HTTPEquivCookieJar: public QNetworkCookieJar { Q_OBJECT public: void mergeHtmlHeadCookies(const QString &htmlCode, const QUrl &url) { static const QRegularExpression cookieContent("^([^\"=; ]+)=([^\"=; ]+).*\\bpath=([^\"=; ]+)", QRegularExpression::CaseInsensitiveOption); int p1 = -1; QRegularExpressionMatch cookieContentRegExpMatch; if ((p1 = htmlCode.toLower().indexOf(QStringLiteral("http-equiv=\"set-cookie\""), 0, Qt::CaseInsensitive)) >= 5 && (p1 = htmlCode.lastIndexOf(QStringLiteral("= 0 && (p1 = htmlCode.indexOf(QStringLiteral("content=\""), p1, Qt::CaseInsensitive)) >= 0 && (cookieContentRegExpMatch = cookieContent.match(htmlCode.mid(p1 + 9, 512))).hasMatch()) { const QString key = cookieContentRegExpMatch.captured(1); const QString value = cookieContentRegExpMatch.captured(2); QList cookies = cookiesForUrl(url); cookies.append(QNetworkCookie(key.toLatin1(), value.toLatin1())); setCookiesFromUrl(cookies, url); } } HTTPEquivCookieJar(QObject *parent = nullptr) : QNetworkCookieJar(parent) { /// nothing } }; QString InternalNetworkAccessManager::userAgentString; InternalNetworkAccessManager::InternalNetworkAccessManager(QObject *parent) : QNetworkAccessManager(parent) { cookieJar = new HTTPEquivCookieJar(this); +#if QT_VERSION < 0x050a00 + qsrand(static_cast(QDateTime::currentDateTime().toMSecsSinceEpoch() % 0x7fffffffl)); +#endif // QT_VERSION } void InternalNetworkAccessManager::mergeHtmlHeadCookies(const QString &htmlCode, const QUrl &url) { Q_ASSERT_X(cookieJar != nullptr, "void InternalNetworkAccessManager::mergeHtmlHeadCookies(const QString &htmlCode, const QUrl &url)", "cookieJar is invalid"); cookieJar->mergeHtmlHeadCookies(htmlCode, url); setCookieJar(cookieJar); } InternalNetworkAccessManager &InternalNetworkAccessManager::instance() { static InternalNetworkAccessManager self; return self; } QNetworkReply *InternalNetworkAccessManager::get(QNetworkRequest &request, const QUrl &oldUrl) { #ifdef HAVE_KF5 /// Query the KDE subsystem if a proxy has to be used /// for the host of a given URL QString proxyHostName = KProtocolManager::proxyForUrl(request.url()); if (!proxyHostName.isEmpty() && proxyHostName != QStringLiteral("DIRECT")) { /// Extract both hostname and port number for proxy proxyHostName = proxyHostName.mid(proxyHostName.indexOf(QStringLiteral("://")) + 3); QStringList proxyComponents = proxyHostName.split(QStringLiteral(":"), QString::SkipEmptyParts); if (proxyComponents.length() == 1) { /// Proxy configuration is missing a port number, /// using 8080 as default proxyComponents << QStringLiteral("8080"); } if (proxyComponents.length() == 2) { /// Set proxy to Qt's NetworkAccessManager setProxy(QNetworkProxy(QNetworkProxy::HttpProxy, proxyComponents[0], proxyComponents[1].toInt())); } } else { /// No proxy to be used, clear previous settings setProxy(QNetworkProxy()); } #else // HAVE_KF5 setProxy(QNetworkProxy()); #endif // HAVE_KF5 if (!request.hasRawHeader(QByteArray("Accept"))) request.setRawHeader(QByteArray("Accept"), QByteArray("text/*, */*;q=0.7")); request.setRawHeader(QByteArray("Accept-Charset"), QByteArray("utf-8, us-ascii, ISO-8859-1;q=0.7, ISO-8859-15;q=0.7, windows-1252;q=0.3")); request.setRawHeader(QByteArray("Accept-Language"), QByteArray("en-US, en;q=0.9")); /// Set 'Referer' and 'Origin' to match the request URL's domain, i.e. URL with empty path QUrl domainUrl = request.url(); domainUrl.setPath(QString()); const QByteArray domain = removeApiKey(domainUrl).toDisplayString().toLatin1(); request.setRawHeader(QByteArray("Referer"), domain); request.setRawHeader(QByteArray("Origin"), domain); request.setRawHeader(QByteArray("User-Agent"), userAgent().toLatin1()); if (oldUrl.isValid()) request.setRawHeader(QByteArray("Referer"), removeApiKey(oldUrl).toDisplayString().toLatin1()); QNetworkReply *reply = QNetworkAccessManager::get(request); /// Log SSL errors connect(reply, &QNetworkReply::sslErrors, this, &InternalNetworkAccessManager::logSslErrors); return reply; } QNetworkReply *InternalNetworkAccessManager::get(QNetworkRequest &request, const QNetworkReply *oldReply) { return get(request, oldReply == nullptr ? QUrl() : oldReply->url()); } QString InternalNetworkAccessManager::userAgent() { /// Various browser strings to "disguise" origin if (userAgentString.isEmpty()) { - if (QRandomGenerator::global()->bounded(0, 1) == 0) { + if (randomGeneratorGlobalBounded(0, 1) == 0) { /// Fake Chrome user agent string static const QString chromeTemplate{QStringLiteral("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/%1.%2 (KHTML, like Gecko) Chrome/%3.%4.%5.%6 Safari/%1.%2")}; - const auto appleWebKitVersionMajor = QRandomGenerator::global()->bounded(537, 856); - const auto appleWebKitVersionMinor = QRandomGenerator::global()->bounded(3, 53); - const auto chromeVersionMajor = QRandomGenerator::global()->bounded(77, 85); - const auto chromeVersionMinor = QRandomGenerator::global()->bounded(0, 4); - const auto chromeVersionBuild = QRandomGenerator::global()->bounded(3793, 8973); - const auto chromeVersionPatch = QRandomGenerator::global()->bounded(53, 673); + const auto appleWebKitVersionMajor = randomGeneratorGlobalBounded(537, 856); + const auto appleWebKitVersionMinor = randomGeneratorGlobalBounded(3, 53); + const auto chromeVersionMajor = randomGeneratorGlobalBounded(77, 85); + const auto chromeVersionMinor = randomGeneratorGlobalBounded(0, 4); + const auto chromeVersionBuild = randomGeneratorGlobalBounded(3793, 8973); + const auto chromeVersionPatch = randomGeneratorGlobalBounded(53, 673); userAgentString = chromeTemplate.arg(appleWebKitVersionMajor).arg(appleWebKitVersionMinor).arg(chromeVersionMajor).arg(chromeVersionMinor).arg(chromeVersionBuild).arg(chromeVersionPatch); } else { /// Fake Firefox user agent string static const QString mozillaTemplate{QStringLiteral("Mozilla/5.0 (Windows NT 6.1; WOW64; rv:%1.%2) Gecko/20100101 Firefox/%1.%2")}; - const auto firefoxVersionMajor = QRandomGenerator::global()->bounded(69, 74); - const auto firefoxVersionMinor = QRandomGenerator::global()->bounded(0, 2); + const auto firefoxVersionMajor = randomGeneratorGlobalBounded(69, 74); + const auto firefoxVersionMinor = randomGeneratorGlobalBounded(0, 2); userAgentString = mozillaTemplate.arg(firefoxVersionMajor).arg(firefoxVersionMinor); } } return userAgentString; } void InternalNetworkAccessManager::setNetworkReplyTimeout(QNetworkReply *reply, int timeOutSec) { QTimer *timer = new QTimer(reply); connect(timer, &QTimer::timeout, this, &InternalNetworkAccessManager::networkReplyTimeout); m_mapTimerToReply.insert(timer, reply); timer->start(timeOutSec * 1000); connect(reply, &QNetworkReply::finished, this, &InternalNetworkAccessManager::networkReplyFinished); } QString InternalNetworkAccessManager::reverseObfuscate(const QByteArray &a) { if (a.length() % 2 != 0 || a.length() == 0) return QString(); QString result; result.reserve(a.length() / 2); for (int p = a.length() - 1; p >= 0; p -= 2) { const QChar c = QLatin1Char(a.at(p) ^ a.at(p - 1)); result.append(c); } return result; } QUrl InternalNetworkAccessManager::removeApiKey(QUrl url) { QUrlQuery urlQuery(url); urlQuery.removeQueryItem(QStringLiteral("apikey")); urlQuery.removeQueryItem(QStringLiteral("api_key")); url.setQuery(urlQuery); return url; } void InternalNetworkAccessManager::networkReplyTimeout() { QTimer *timer = static_cast(sender()); timer->stop(); QNetworkReply *reply = m_mapTimerToReply[timer]; if (reply != nullptr) { qCWarning(LOG_KBIBTEX_NETWORKING) << "Timeout on reply to " << removeApiKey(reply->url()).toDisplayString(); reply->close(); m_mapTimerToReply.remove(timer); } } void InternalNetworkAccessManager::networkReplyFinished() { QNetworkReply *reply = static_cast(sender()); QTimer *timer = m_mapTimerToReply.key(reply, nullptr); if (timer != nullptr) { disconnect(timer, &QTimer::timeout, this, &InternalNetworkAccessManager::networkReplyTimeout); timer->stop(); m_mapTimerToReply.remove(timer); } } void InternalNetworkAccessManager::logSslErrors(const QList &errors) { QNetworkReply *reply = static_cast(sender()); qCWarning(LOG_KBIBTEX_NETWORKING) << QStringLiteral("Got the following SSL errors when querying the following URL: ") << removeApiKey(reply->url()).toDisplayString(); for (const QSslError &error : errors) qCWarning(LOG_KBIBTEX_NETWORKING) << QStringLiteral(" * ") + error.errorString() << "; Code: " << static_cast(error.error()); } #include "internalnetworkaccessmanager.moc" diff --git a/src/networking/onlinesearch/onlinesearchabstract.cpp b/src/networking/onlinesearch/onlinesearchabstract.cpp index 9dc5b4d1..b42d9bde 100644 --- a/src/networking/onlinesearch/onlinesearchabstract.cpp +++ b/src/networking/onlinesearch/onlinesearchabstract.cpp @@ -1,604 +1,612 @@ /*************************************************************************** * Copyright (C) 2004-2020 by Thomas Fischer * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, see . * ***************************************************************************/ #include "onlinesearchabstract.h" #include #include #include #include #include #include #ifdef HAVE_KF5 #include #include #endif // HAVE_KF5 +#if QT_VERSION >= 0x050a00 #include +#endif // QT_VERSION #ifdef HAVE_QTWIDGETS #include #endif // HAVE_QTWIDGETS #ifdef HAVE_KF5 #include #include #endif // HAVE_KF5 #include #include #include "internalnetworkaccessmanager.h" #include "onlinesearchabstract_p.h" #include "faviconlocator.h" #include "logging_networking.h" +#if QT_VERSION >= 0x050a00 +#define randomGeneratorGlobalGenerate() QRandomGenerator::global()->generate() +#else // QT_VERSION +#define randomGeneratorGlobalGenerate() (qrand()) +#endif // QT_VERSION + const int OnlineSearchAbstract::resultNoError = 0; const int OnlineSearchAbstract::resultCancelled = 0; /// may get redefined in the future! const int OnlineSearchAbstract::resultUnspecifiedError = 1; const int OnlineSearchAbstract::resultAuthorizationRequired = 2; const int OnlineSearchAbstract::resultNetworkError = 3; const int OnlineSearchAbstract::resultInvalidArguments = 4; const char *OnlineSearchAbstract::httpUnsafeChars = "%:/=+$?&\0"; #ifdef HAVE_QTWIDGETS OnlineSearchAbstract::Form::Private::Private() : config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc"))) { /// nothing } QStringList OnlineSearchAbstract::Form::Private::authorLastNames(const Entry &entry) { const Encoder &encoder = Encoder::instance(); const Value v = entry[Entry::ftAuthor]; QStringList result; result.reserve(v.size()); for (const QSharedPointer &vi : v) { QSharedPointer p = vi.dynamicCast(); if (!p.isNull()) result.append(encoder.convertToPlainAscii(p->lastName())); } return result; } QString OnlineSearchAbstract::Form::Private::guessFreeText(const Entry &entry) { /// If there is a DOI value in this entry, use it as free text static const QStringList doiKeys = {Entry::ftDOI, Entry::ftUrl}; for (const QString &doiKey : doiKeys) if (!entry.value(doiKey).isEmpty()) { const QString text = PlainTextValue::text(entry[doiKey]); const QRegularExpressionMatch doiRegExpMatch = KBibTeX::doiRegExp.match(text); if (doiRegExpMatch.hasMatch()) return doiRegExpMatch.captured(0); } /// If there is no free text yet (e.g. no DOI number), try to identify an arXiv eprint number static const QStringList arxivKeys = {QStringLiteral("eprint"), Entry::ftNumber}; for (const QString &arxivKey : arxivKeys) if (!entry.value(arxivKey).isEmpty()) { const QString text = PlainTextValue::text(entry[arxivKey]); const QRegularExpressionMatch arXivRegExpMatch = KBibTeX::arXivRegExpWithPrefix.match(text); if (arXivRegExpMatch.hasMatch()) return arXivRegExpMatch.captured(1); } return QString(); } #endif // HAVE_QTWIDGETS OnlineSearchAbstract::OnlineSearchAbstract(QObject *parent) : QObject(parent), m_hasBeenCanceled(false), numSteps(0), curStep(0), m_previousBusyState(false), m_delayedStoppedSearchReturnCode(0) { m_parent = parent; } #ifdef HAVE_QTWIDGETS QIcon OnlineSearchAbstract::icon(QListWidgetItem *listWidgetItem) { FavIconLocator *fil = new FavIconLocator(homepage(), this); connect(fil, &FavIconLocator::gotIcon, this, [listWidgetItem](const QIcon &icon) { listWidgetItem->setIcon(icon); }); return fil->icon(); } OnlineSearchAbstract::Form *OnlineSearchAbstract::customWidget(QWidget *) { return nullptr; } void OnlineSearchAbstract::startSearchFromForm() { m_hasBeenCanceled = false; curStep = numSteps = 0; delayedStoppedSearch(resultNoError); } #endif // HAVE_QTWIDGETS QString OnlineSearchAbstract::name() { if (m_name.isEmpty()) { static const QRegularExpression invalidChars(QStringLiteral("[^-a-z0-9]"), QRegularExpression::CaseInsensitiveOption); m_name = label().remove(invalidChars); } return m_name; } bool OnlineSearchAbstract::busy() const { return numSteps > 0 && curStep < numSteps; } void OnlineSearchAbstract::cancel() { m_hasBeenCanceled = true; curStep = numSteps = 0; refreshBusyProperty(); } QStringList OnlineSearchAbstract::splitRespectingQuotationMarks(const QString &text) { int p1 = 0, p2, max = text.length(); QStringList result; while (p1 < max) { while (text[p1] == ' ') ++p1; p2 = p1; if (text[p2] == '"') { ++p2; while (p2 < max && text[p2] != '"') ++p2; } else while (p2 < max && text[p2] != ' ') ++p2; result << text.mid(p1, p2 - p1 + 1).simplified(); p1 = p2 + 1; } return result; } bool OnlineSearchAbstract::handleErrors(QNetworkReply *reply) { QUrl url; return handleErrors(reply, url); } bool OnlineSearchAbstract::handleErrors(QNetworkReply *reply, QUrl &newUrl) { /// The URL to be shown or logged shall not contain any API key const QUrl urlToShow = InternalNetworkAccessManager::removeApiKey(reply->url()); newUrl = QUrl(); if (m_hasBeenCanceled) { stopSearch(resultCancelled); return false; } else if (reply->error() != QNetworkReply::NoError) { m_hasBeenCanceled = true; const QString errorString = reply->errorString(); qCWarning(LOG_KBIBTEX_NETWORKING) << "Search using" << label() << "failed (error code" << reply->error() << "," << errorString << "), HTTP code" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() << ":" << reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toByteArray() << ") for URL" << urlToShow.toDisplayString(); const QNetworkRequest &request = reply->request(); /// Dump all HTTP headers that were sent with the original request (except for API keys) const QList rawHeadersSent = request.rawHeaderList(); for (const QByteArray &rawHeaderName : rawHeadersSent) { if (rawHeaderName.toLower().contains("apikey") || rawHeaderName.toLower().contains("api-key")) continue; ///< skip dumping header values containing an API key qCDebug(LOG_KBIBTEX_NETWORKING) << "SENT " << rawHeaderName << ":" << request.rawHeader(rawHeaderName); } /// Dump all HTTP headers that were received const QList rawHeadersReceived = reply->rawHeaderList(); for (const QByteArray &rawHeaderName : rawHeadersReceived) { if (rawHeaderName.toLower().contains("apikey") || rawHeaderName.toLower().contains("api-key")) continue; ///< skip dumping header values containing an API key qCDebug(LOG_KBIBTEX_NETWORKING) << "RECVD " << rawHeaderName << ":" << reply->rawHeader(rawHeaderName); } #ifdef HAVE_KF5 sendVisualNotification(errorString.isEmpty() ? i18n("Searching '%1' failed for unknown reason.", label()) : i18n("Searching '%1' failed with error message:\n\n%2", label(), errorString), label(), QStringLiteral("kbibtex"), 7 * 1000); #endif // HAVE_KF5 int resultCode = resultUnspecifiedError; if (reply->error() == QNetworkReply::AuthenticationRequiredError || reply->error() == QNetworkReply::ProxyAuthenticationRequiredError) resultCode = resultAuthorizationRequired; else if (reply->error() == QNetworkReply::HostNotFoundError || reply->error() == QNetworkReply::TimeoutError) resultCode = resultNetworkError; stopSearch(resultCode); return false; } /** * Check the reply for various problems that might point to * more severe issues. Remember: those are only indicators * to problems which have to be handled elsewhere (therefore, * returning 'true' is totally ok here). */ if (reply->attribute(QNetworkRequest::RedirectionTargetAttribute).isValid()) { newUrl = reply->url().resolved(reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl()); } else if (reply->size() == 0) qCWarning(LOG_KBIBTEX_NETWORKING) << "Search using" << label() << "on url" << urlToShow.toDisplayString() << "returned no data"; return true; } QString OnlineSearchAbstract::htmlAttribute(const QString &htmlCode, const int startPos, const QString &attribute) const { const int endPos = htmlCode.indexOf(QLatin1Char('>'), startPos); if (endPos < 0) return QString(); ///< no closing angle bracket found const QString attributePattern = QString(QStringLiteral(" %1=")).arg(attribute); const int attributePatternPos = htmlCode.indexOf(attributePattern, startPos, Qt::CaseInsensitive); if (attributePatternPos < 0 || attributePatternPos > endPos) return QString(); ///< attribute not found within limits const int attributePatternLen = attributePattern.length(); const int openingQuotationMarkPos = attributePatternPos + attributePatternLen; const QChar quotationMark = htmlCode[openingQuotationMarkPos]; if (quotationMark != QLatin1Char('"') && quotationMark != QLatin1Char('\'')) { /// No valid opening quotation mark found int spacePos = openingQuotationMarkPos; while (spacePos < endPos && !htmlCode[spacePos].isSpace()) ++spacePos; if (spacePos > endPos) return QString(); ///< no closing space found return htmlCode.mid(openingQuotationMarkPos, spacePos - openingQuotationMarkPos); } else { /// Attribute has either single or double quotation marks const int closingQuotationMarkPos = htmlCode.indexOf(quotationMark, openingQuotationMarkPos + 1); if (closingQuotationMarkPos < 0 || closingQuotationMarkPos > endPos) return QString(); ///< closing quotation mark not found within limits return htmlCode.mid(openingQuotationMarkPos + 1, closingQuotationMarkPos - openingQuotationMarkPos - 1); } } bool OnlineSearchAbstract::htmlAttributeIsSelected(const QString &htmlCode, const int startPos, const QString &attribute) const { const int endPos = htmlCode.indexOf(QLatin1Char('>'), startPos); if (endPos < 0) return false; ///< no closing angle bracket found const QString attributePattern = QStringLiteral(" ") + attribute; const int attributePatternPos = htmlCode.indexOf(attributePattern, startPos, Qt::CaseInsensitive); if (attributePatternPos < 0 || attributePatternPos > endPos) return false; ///< attribute not found within limits const int attributePatternLen = attributePattern.length(); const QChar nextAfterAttributePattern = htmlCode[attributePatternPos + attributePatternLen]; if (nextAfterAttributePattern.isSpace() || nextAfterAttributePattern == QLatin1Char('>') || nextAfterAttributePattern == QLatin1Char('/')) /// No value given for attribute (old-style HTML), so assuming it means checked/selected return true; else if (nextAfterAttributePattern == QLatin1Char('=')) { /// Expecting value to attribute, so retrieve it and check for 'selected' or 'checked' const QString attributeValue = htmlAttribute(htmlCode, attributePatternPos, attribute).toLower(); return attributeValue == QStringLiteral("selected") || attributeValue == QStringLiteral("checked"); } /// Reaching this point only if HTML code is invalid return false; } #ifdef HAVE_KF5 /** * Display a passive notification popup using the D-Bus interface. * Copied from KDialog with modifications. */ void OnlineSearchAbstract::sendVisualNotification(const QString &text, const QString &title, const QString &icon, int timeout) { static const QString dbusServiceName = QStringLiteral("org.freedesktop.Notifications"); static const QString dbusInterfaceName = QStringLiteral("org.freedesktop.Notifications"); static const QString dbusPath = QStringLiteral("/org/freedesktop/Notifications"); // check if service already exists on plugin instantiation QDBusConnectionInterface *interface = QDBusConnection::sessionBus().interface(); if (interface == nullptr || !interface->isServiceRegistered(dbusServiceName)) { return; } if (timeout <= 0) timeout = 10 * 1000; QDBusMessage m = QDBusMessage::createMethodCall(dbusServiceName, dbusPath, dbusInterfaceName, QStringLiteral("Notify")); const QList args {QStringLiteral("kdialog"), 0U, icon, title, text, QStringList(), QVariantMap(), timeout}; m.setArguments(args); QDBusMessage replyMsg = QDBusConnection::sessionBus().call(m); if (replyMsg.type() == QDBusMessage::ReplyMessage) { if (!replyMsg.arguments().isEmpty()) { return; } // Not displaying any error messages as this is optional for kdialog // and KPassivePopup is a perfectly valid fallback. //else { // qCDebug(LOG_KBIBTEX_NETWORKING) << "Error: received reply with no arguments."; //} } else if (replyMsg.type() == QDBusMessage::ErrorMessage) { //qCDebug(LOG_KBIBTEX_NETWORKING) << "Error: failed to send D-Bus message"; //qCDebug(LOG_KBIBTEX_NETWORKING) << replyMsg; } else { //qCDebug(LOG_KBIBTEX_NETWORKING) << "Unexpected reply type"; } } #endif // HAVE_KF5 QString OnlineSearchAbstract::encodeURL(QString rawText) { const char *cur = httpUnsafeChars; while (*cur != '\0') { rawText = rawText.replace(QChar(*cur), '%' + QString::number(*cur, 16)); ++cur; } rawText = rawText.replace(QLatin1Char(' '), QLatin1Char('+')); return rawText; } QString OnlineSearchAbstract::decodeURL(QString rawText) { static const QRegularExpression mimeRegExp(QStringLiteral("%([0-9A-Fa-f]{2})")); QRegularExpressionMatch mimeRegExpMatch; while ((mimeRegExpMatch = mimeRegExp.match(rawText)).hasMatch()) { bool ok = false; QChar c(mimeRegExpMatch.captured(1).toInt(&ok, 16)); if (ok) rawText = rawText.replace(mimeRegExpMatch.captured(0), c); } rawText = rawText.replace(QStringLiteral("&"), QStringLiteral("&")).replace(QLatin1Char('+'), QStringLiteral(" ")); return rawText; } QMap OnlineSearchAbstract::formParameters(const QString &htmlText, int startPos) const { /// how to recognize HTML tags static const QString formTagEnd = QStringLiteral(""); static const QString inputTagBegin = QStringLiteral(""); static const QString optionTagBegin = QStringLiteral("