From 497a469a8b039546d40ca47d5face2be4f5f064f Mon Sep 17 00:00:00 2001 From: Martin Rotter Date: Thu, 26 Oct 2017 13:14:44 +0200 Subject: [PATCH] local http server for nonwebengine oauth. --- resources/binaries | 2 +- resources/graphics/misc/feedly.png | Bin 5352 -> 0 bytes resources/rssguard.qrc | 1 - rssguard.pro | 6 +- src/gui/styleditemdelegatewithoutfocus.h | 14 + src/network-web/oauth2service.cpp | 61 +++- src/network-web/oauth2service.h | 21 +- src/network-web/oauthhttphandler.cpp | 276 ++++++++++++++++++ src/network-web/oauthhttphandler.h | 69 +++++ src/services/gmail/gmailentrypoint.cpp | 7 - .../gmail/gui/formeditgmailaccount.cpp | 2 +- .../gmail/network/gmailnetworkfactory.cpp | 6 +- .../gui/formeditinoreaderaccount.cpp | 2 +- .../inoreader/inoreaderentrypoint.cpp | 7 - .../network/inoreadernetworkfactory.cpp | 2 +- 15 files changed, 434 insertions(+), 42 deletions(-) delete mode 100755 resources/graphics/misc/feedly.png create mode 100644 src/network-web/oauthhttphandler.cpp create mode 100644 src/network-web/oauthhttphandler.h diff --git a/resources/binaries b/resources/binaries index 4a01edaec..ae7084718 160000 --- a/resources/binaries +++ b/resources/binaries @@ -1 +1 @@ -Subproject commit 4a01edaec7d67d3b2ae81aeea2a3c876216fbab8 +Subproject commit ae7084718c41afc01919779e58cd449e0eebd401 diff --git a/resources/graphics/misc/feedly.png b/resources/graphics/misc/feedly.png deleted file mode 100755 index c1a435bf76b9f9a50940f1ebf40cea2574e3d4e6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5352 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/rssguard_plain.png graphics/misc/adblock.png graphics/misc/adblock-disabled.png - graphics/misc/feedly.png graphics/misc/gmail.png graphics/misc/image-placeholder.png graphics/misc/inoreader.png diff --git a/rssguard.pro b/rssguard.pro index a79ca0e86..3baa022f2 100755 --- a/rssguard.pro +++ b/rssguard.pro @@ -563,11 +563,13 @@ equals(USE_WEBENGINE, true) { else { HEADERS += src/gui/messagepreviewer.h \ src/gui/messagetextbrowser.h \ - src/gui/newspaperpreviewer.h + src/gui/newspaperpreviewer.h \ + src/network-web/oauthhttphandler.h SOURCES += src/gui/messagepreviewer.cpp \ src/gui/messagetextbrowser.cpp \ - src/gui/newspaperpreviewer.cpp + src/gui/newspaperpreviewer.cpp \ + src/network-web/oauthhttphandler.cpp FORMS += src/gui/messagepreviewer.ui \ src/gui/newspaperpreviewer.ui diff --git a/src/gui/styleditemdelegatewithoutfocus.h b/src/gui/styleditemdelegatewithoutfocus.h index 153b81937..867e1e480 100755 --- a/src/gui/styleditemdelegatewithoutfocus.h +++ b/src/gui/styleditemdelegatewithoutfocus.h @@ -15,6 +15,20 @@ class StyledItemDelegateWithoutFocus : public QStyledItemDelegate { explicit StyledItemDelegateWithoutFocus(QObject* parent = 0); virtual ~StyledItemDelegateWithoutFocus(); + QSize sizeHint ( const QStyleOptionViewItem& option, const QModelIndex& index ) const + { + QSize siz = QStyledItemDelegate::sizeHint(option, index); + + /* QStyleOptionViewItem opt = option; + + initStyleOption(&opt, index); + QStyle* style = widget ? widget->style() : QApplication::style(); + + return style->sizeFromContents(QStyle::CT_ItemViewItem, &opt, QSize(), widget);*/ + + return siz; + } + void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const; }; diff --git a/src/network-web/oauth2service.cpp b/src/network-web/oauth2service.cpp index 8b96e86fb..d4f7a3209 100755 --- a/src/network-web/oauth2service.cpp +++ b/src/network-web/oauth2service.cpp @@ -26,10 +26,15 @@ #include "definitions/definitions.h" #include "miscellaneous/application.h" +#include "network-web/webfactory.h" #include "services/inoreader/definitions.h" #if defined(USE_WEBENGINE) #include "gui/dialogs/oauthlogin.h" +#else +#include "network-web/oauthhttphandler.h" + +Q_GLOBAL_STATIC(OAuthHttpHandler, qz_silent_acmanager) #endif #include @@ -38,21 +43,34 @@ #include #include -OAuth2Service::OAuth2Service(QString authUrl, QString tokenUrl, QString clientId, - QString clientSecret, QString scope, QObject* parent) +OAuth2Service::OAuth2Service(const QString& id_string, const QString& auth_url, const QString& token_url, const QString& client_id, + const QString& client_secret, const QString& scope, QObject* parent) : QObject(parent), m_timerId(-1), m_tokensExpireIn(QDateTime()) { + if (id_string.isEmpty()) { + m_id = "somerandomstring"; + } + else { + m_id = id_string; + } + m_redirectUrl = QSL(LOCALHOST_ADDRESS); m_tokenGrantType = QSL("authorization_code"); - m_tokenUrl = QUrl(tokenUrl); - m_authUrl = authUrl; + m_tokenUrl = QUrl(token_url); + m_authUrl = auth_url; - m_clientId = clientId; - m_clientSecret = clientSecret; + m_clientId = client_id; + m_clientSecret = client_secret; m_scope = scope; connect(&m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(tokenRequestFinished(QNetworkReply*))); - connect(this, &OAuth2Service::authCodeObtained, this, &OAuth2Service::retrieveAccessToken); + +#if !defined(USE_WEBENGINE) + connect(handler(), &OAuthHttpHandler::authGranted, this, &OAuth2Service::retrieveAccessToken); + connect(handler(), &OAuthHttpHandler::authRejected, [this](const QString& error_description) { + emit authFailed(); + }); +#endif } QString OAuth2Service::bearer() { @@ -105,6 +123,21 @@ void OAuth2Service::timerEvent(QTimerEvent* event) { QObject::timerEvent(event); } +QString OAuth2Service::id() const { + return m_id; +} + +void OAuth2Service::setId(const QString& id) { + m_id = id; +} + +#if !defined(USE_WEBENGINE) +OAuthHttpHandler* OAuth2Service::handler() { + return qz_silent_acmanager(); +} + +#endif + void OAuth2Service::retrieveAccessToken(QString auth_code) { QNetworkRequest networkRequest; @@ -155,7 +188,6 @@ void OAuth2Service::tokenRequestFinished(QNetworkReply* network_reply) { QByteArray repl = network_reply->readAll(); QJsonDocument json_document = QJsonDocument::fromJson(repl); QJsonObject root_obj = json_document.object(); - auto cod = network_reply->error(); qDebug() << "Token response:" << json_document.toJson(); @@ -276,15 +308,16 @@ void OAuth2Service::killRefreshTimer() { void OAuth2Service::retrieveAuthCode() { QString auth_url = m_authUrl + QString("?client_id=%1&scope=%2&" - "redirect_uri=%3&response_type=code&state=abcdef&" + "redirect_uri=%3&response_type=code&state=%4&" "prompt=consent&access_type=offline").arg(m_clientId, m_scope, - m_redirectUrl); + m_redirectUrl, + m_id); #if defined(USE_WEBENGINE) OAuthLogin login_page(qApp->mainFormWidget()); - connect(&login_page, &OAuthLogin::authGranted, this, &OAuth2Service::authCodeObtained); + connect(&login_page, &OAuthLogin::authGranted, this, &OAuth2Service::retrieveAccessToken); connect(&login_page, &OAuthLogin::authRejected, [this]() { logout(); emit authFailed(); @@ -295,7 +328,9 @@ void OAuth2Service::retrieveAuthCode() { QSystemTrayIcon::MessageIcon::Information); login_page.login(auth_url, m_redirectUrl); -#endif +#else - // TODO: For non-webengine version, user http-server and login via external browser. + // We run login URL in external browser, response is caught by light HTTP server. + qApp->web()->openUrlInExternalBrowser(auth_url); +#endif } diff --git a/src/network-web/oauth2service.h b/src/network-web/oauth2service.h index 7361edaad..382ce8c23 100755 --- a/src/network-web/oauth2service.h +++ b/src/network-web/oauth2service.h @@ -29,12 +29,16 @@ #include "network-web/silentnetworkaccessmanager.h" +#if !defined(USE_WEBENGINE) +#include "network-web/oauthhttphandler.h" +#endif + class OAuth2Service : public QObject { Q_OBJECT public: - explicit OAuth2Service(QString authUrl, QString tokenUrl, QString clientId, - QString clientSecret, QString scope, QObject* parent = 0); + explicit OAuth2Service(const QString& id_string, const QString& auth_url, const QString& token_url, + const QString& client_id, const QString& client_secret, const QString& scope, QObject* parent = 0); // Returns bearer HTTP header value. // NOTE: Only call this if isFullyLoggedIn() @@ -65,6 +69,9 @@ class OAuth2Service : public QObject { QString accessToken() const; void setAccessToken(const QString& access_token); + QString id() const; + void setId(const QString& id); + signals: void tokensReceived(QString access_token, QString refresh_token, int expires_in); void tokensRetrieveError(QString error, QString error_description); @@ -72,9 +79,6 @@ class OAuth2Service : public QObject { // User failed to authenticate or rejected it. void authFailed(); - // User enabled access. - void authCodeObtained(QString auth_code); - public slots: void retrieveAuthCode(); void retrieveAccessToken(QString auth_code); @@ -100,6 +104,7 @@ class OAuth2Service : public QObject { void timerEvent(QTimerEvent* event); private: + QString m_id; int m_timerId; QDateTime m_tokensExpireIn; QString m_accessToken; @@ -112,6 +117,12 @@ class OAuth2Service : public QObject { QString m_authUrl; QString m_scope; SilentNetworkAccessManager m_networkManager; + +#if !defined(USE_WEBENGINE) + + // Returns pointer to global silent network manager + static OAuthHttpHandler* handler(); +#endif }; #endif // OAUTH2SERVICE_H diff --git a/src/network-web/oauthhttphandler.cpp b/src/network-web/oauthhttphandler.cpp new file mode 100644 index 000000000..e6bd98049 --- /dev/null +++ b/src/network-web/oauthhttphandler.cpp @@ -0,0 +1,276 @@ +// For license of this file, see /LICENSE.md. + +#include "network-web/oauthhttphandler.h" + +#include "definitions/definitions.h" +#include "miscellaneous/application.h" + +#include + +#include +#include + +OAuthHttpHandler::OAuthHttpHandler(QObject* parent) : QObject(parent) { + m_text = tr("You can close this window now. Go back to %1").arg(APP_NAME); + + connect(&m_httpServer, &QTcpServer::newConnection, this, &OAuthHttpHandler::clientConnected); + + if (!m_httpServer.listen(m_listenAddress, 80)) { + qCritical("OAuth HTTP handler: Failed to start listening."); + } +} + +OAuthHttpHandler::~OAuthHttpHandler() { + if (m_httpServer.isListening()) { + m_httpServer.close(); + } +} + +void OAuthHttpHandler::clientConnected() { + QTcpSocket* socket = m_httpServer.nextPendingConnection(); + + QObject::connect(socket, &QTcpSocket::disconnected, socket, &QTcpSocket::deleteLater); + QObject::connect(socket, &QTcpSocket::readyRead, [this, socket]() { + readReceivedData(socket); + }); +} + +void OAuthHttpHandler::handleRedirection(const QVariantMap& data) { + if (data.isEmpty()) { + return; + } + + const QString error = data.value(QSL("error")).toString(); + const QString code = data.value(QSL("code")).toString(); + const QString received_state = data.value(QSL("state")).toString(); + + if (error.size()) { + const QString uri = data.value(QSL("error_uri")).toString(); + const QString description = data.value(QSL("error_description")).toString(); + + qWarning("OAuth HTTP handler: AuthenticationError: %s(%s): %s", qPrintable(error), qPrintable(uri), qPrintable(description)); + emit authRejected(description); + } + else if (code.isEmpty()) { + qWarning("OAuth HTTP handler: AuthenticationError: Code not received"); + emit authRejected(QSL("AuthenticationError: Code not received")); + } + else if (received_state.isEmpty()) { + qWarning("OAuth HTTP handler: State not received"); + emit authRejected(QSL("State not received")); + } + else { + emit authGranted(code); + } +} + +void OAuthHttpHandler::answerClient(QTcpSocket* socket, const QUrl& url) { + if (!url.path().remove(QL1C('/')).isEmpty()) { + qWarning("OAuth HTTP handler: Invalid request: %s", qPrintable(url.toString())); + } + else { + QVariantMap received_data; + const QUrlQuery query(url.query()); + const auto items = query.queryItems(); + + for (auto it = items.begin(), end = items.end(); it != end; ++it) { + received_data.insert(it->first, it->second); + } + + handleRedirection(received_data); + + const QByteArray html = QByteArrayLiteral("") + + qApp->applicationName().toUtf8() + + QByteArrayLiteral("") + + m_text.toUtf8() + + QByteArrayLiteral(""); + const QByteArray html_size = QString::number(html.size()).toUtf8(); + const QByteArray reply_message = QByteArrayLiteral("HTTP/1.0 200 OK \r\n" + "Content-Type: text/html; " + "charset=\"utf-8\"\r\n" + "Content-Length: ") + html_size + + QByteArrayLiteral("\r\n\r\n") + html; + + socket->write(reply_message); + } + + socket->disconnectFromHost(); +} + +void OAuthHttpHandler::readReceivedData(QTcpSocket* socket) { + if (!m_connectedClients.contains(socket)) { + m_connectedClients[socket].m_port = m_httpServer.serverPort(); + } + + QHttpRequest* request = &m_connectedClients[socket]; + bool error = false; + + if (Q_LIKELY(request->m_state == QHttpRequest::State::ReadingMethod)) { + if (Q_UNLIKELY(error = !request->readMethod(socket))) { + qWarning("OAuth HTTP handler: Invalid dethod"); + } + } + + if (Q_LIKELY(!error && request->m_state == QHttpRequest::State::ReadingUrl)) { + if (Q_UNLIKELY(error = !request->readUrl(socket))) { + qWarning("OAuth HTTP handler: Invalid URL"); + } + } + + if (Q_LIKELY(!error && request->m_state == QHttpRequest::State::ReadingStatus)) { + if (Q_UNLIKELY(error = !request->readStatus(socket))) { + qWarning("OAuth HTTP handler: Invalid status"); + } + } + + if (Q_LIKELY(!error && request->m_state == QHttpRequest::State::ReadingHeader)) { + if (Q_UNLIKELY(error = !request->readHeader(socket))) { + qWarning("OAuth HTTP handler: Invalid header"); + } + } + + if (error) { + socket->disconnectFromHost(); + m_connectedClients.remove(socket); + } + else if (!request->m_url.isEmpty()) { + Q_ASSERT(request->m_state != QHttpRequest::State::ReadingUrl); + + answerClient(socket, request->m_url); + m_connectedClients.remove(socket); + } +} + +bool OAuthHttpHandler::QHttpRequest::readMethod(QTcpSocket* socket) { + bool finished = false; + + while (socket->bytesAvailable() && !finished) { + const auto c = socket->read(1).at(0); + + if (std::isupper(c) && m_fragment.size() < 6) { + m_fragment += c; + } + else { + finished = true; + } + } + + if (finished) { + if (m_fragment == "HEAD") { + m_method = Method::Head; + } + else if (m_fragment == "GET") { + m_method = Method::Get; + } + else if (m_fragment == "PUT") { + m_method = Method::Put; + } + else if (m_fragment == "POST") { + m_method = Method::Post; + } + else if (m_fragment == "DELETE") { + m_method = Method::Delete; + } + else { + qWarning("OAuth HTTP handler: Invalid operation %s", m_fragment.data()); + } + + m_state = State::ReadingUrl; + m_fragment.clear(); + + return m_method != Method::Unknown; + } + + return true; +} + +bool OAuthHttpHandler::QHttpRequest::readUrl(QTcpSocket* socket) { + bool finished = false; + + while (socket->bytesAvailable() && !finished) { + const auto c = socket->read(1).at(0); + + if (std::isspace(c)) { + finished = true; + } + else { + m_fragment += c; + } + } + + if (finished) { + if (!m_fragment.startsWith("/")) { + qWarning("OAuth HTTP handler: Invalid URL path %s", m_fragment.constData()); + return false; + } + + m_url.setUrl(QStringLiteral("http://localhost:") + QString::number(m_port) + QString::fromUtf8(m_fragment)); + m_state = State::ReadingStatus; + + if (!m_url.isValid()) { + qWarning("OAuth HTTP handler: Invalid URL %s", m_fragment.constData()); + return false; + } + + m_fragment.clear(); + return true; + } + + return true; +} + +bool OAuthHttpHandler::QHttpRequest::readStatus(QTcpSocket* socket) { + bool finished = false; + + while (socket->bytesAvailable() && !finished) { + m_fragment += socket->read(1); + + if (m_fragment.endsWith("\r\n")) { + finished = true; + m_fragment.resize(m_fragment.size() - 2); + } + } + + if (finished) { + if (!std::isdigit(m_fragment.at(m_fragment.size() - 3)) || !std::isdigit(m_fragment.at(m_fragment.size() - 1))) { + qWarning("OAuth HTTP handler: Invalid version"); + return false; + } + + m_version = qMakePair(m_fragment.at(m_fragment.size() - 3) - '0', m_fragment.at(m_fragment.size() - 1) - '0'); + m_state = State::ReadingHeader; + m_fragment.clear(); + } + + return true; +} + +bool OAuthHttpHandler::QHttpRequest::readHeader(QTcpSocket* socket) { + while (socket->bytesAvailable()) { + m_fragment += socket->read(1); + + if (m_fragment.endsWith("\r\n")) { + if (m_fragment == "\r\n") { + m_state = State::ReadingBody; + m_fragment.clear(); + return true; + } + else { + m_fragment.chop(2); + const int index = m_fragment.indexOf(':'); + + if (index == -1) { + return false; + } + + const QByteArray key = m_fragment.mid(0, index).trimmed(); + const QByteArray value = m_fragment.mid(index + 1).trimmed(); + + m_headers.insert(key, value); + m_fragment.clear(); + } + } + } + + return false; +} diff --git a/src/network-web/oauthhttphandler.h b/src/network-web/oauthhttphandler.h new file mode 100644 index 000000000..9cdbe0f32 --- /dev/null +++ b/src/network-web/oauthhttphandler.h @@ -0,0 +1,69 @@ +// For license of this file, see /LICENSE.md. + +#ifndef OAUTHHTTPHANDLER_H +#define OAUTHHTTPHANDLER_H + +#include + +#include +#include + +class OAuthHttpHandler : public QObject { + Q_OBJECT + + public: + explicit OAuthHttpHandler(QObject* parent = nullptr); + virtual ~OAuthHttpHandler(); + + signals: + void authRejected(const QString& error_description); + void authGranted(const QString& auth_code); + + private slots: + void clientConnected(); + + private: + void handleRedirection(const QVariantMap& data); + void answerClient(QTcpSocket* socket, const QUrl& url); + void readReceivedData(QTcpSocket* socket); + + private: + struct QHttpRequest { + bool readMethod(QTcpSocket* socket); + bool readUrl(QTcpSocket* socket); + bool readStatus(QTcpSocket* socket); + bool readHeader(QTcpSocket* socket); + + enum class State { + ReadingMethod, + ReadingUrl, + ReadingStatus, + ReadingHeader, + ReadingBody, + AllDone + } m_state = State::ReadingMethod; + + enum class Method { + Unknown, + Head, + Get, + Put, + Post, + Delete, + } m_method = Method::Unknown; + + quint16 m_port = 0; + QByteArray m_fragment; + QUrl m_url; + + QPair m_version; + QMap m_headers; + }; + + QMap m_connectedClients; + QTcpServer m_httpServer; + QHostAddress m_listenAddress = QHostAddress::LocalHost; + QString m_text; +}; + +#endif // OAUTHHTTPHANDLER_H diff --git a/src/services/gmail/gmailentrypoint.cpp b/src/services/gmail/gmailentrypoint.cpp index cbf3e27bf..ac1182c57 100755 --- a/src/services/gmail/gmailentrypoint.cpp +++ b/src/services/gmail/gmailentrypoint.cpp @@ -13,16 +13,9 @@ #include ServiceRoot* GmailEntryPoint::createNewRoot() const { -#if defined(USE_WEBENGINE) FormEditGmailAccount form_acc(qApp->mainFormWidget()); return form_acc.execForCreate(); -#else - QMessageBox::warning(qApp->mainFormWidget(), - QObject::tr("Not supported"), - QObject::tr("This plugin is not supported in NonWebEngine variant of this program.")); - return nullptr; -#endif } QList GmailEntryPoint::initializeSubtree() const { diff --git a/src/services/gmail/gui/formeditgmailaccount.cpp b/src/services/gmail/gui/formeditgmailaccount.cpp index d602d369d..5d48eddff 100755 --- a/src/services/gmail/gui/formeditgmailaccount.cpp +++ b/src/services/gmail/gui/formeditgmailaccount.cpp @@ -10,7 +10,7 @@ #include "services/gmail/gmailserviceroot.h" FormEditGmailAccount::FormEditGmailAccount(QWidget* parent) : QDialog(parent), - m_oauth(new OAuth2Service(GMAIL_OAUTH_AUTH_URL, GMAIL_OAUTH_TOKEN_URL, + m_oauth(new OAuth2Service(QString(), GMAIL_OAUTH_AUTH_URL, GMAIL_OAUTH_TOKEN_URL, QString(), QString(), GMAIL_OAUTH_SCOPE)), m_editableRoot(nullptr) { m_ui.setupUi(this); diff --git a/src/services/gmail/network/gmailnetworkfactory.cpp b/src/services/gmail/network/gmailnetworkfactory.cpp index 4cf49a5c7..faab9a383 100755 --- a/src/services/gmail/network/gmailnetworkfactory.cpp +++ b/src/services/gmail/network/gmailnetworkfactory.cpp @@ -26,7 +26,7 @@ GmailNetworkFactory::GmailNetworkFactory(QObject* parent) : QObject(parent), m_service(nullptr), m_username(QString()), m_batchSize(GMAIL_DEFAULT_BATCH_SIZE), - m_oauth2(new OAuth2Service(GMAIL_OAUTH_AUTH_URL, GMAIL_OAUTH_TOKEN_URL, + m_oauth2(new OAuth2Service(QString(), GMAIL_OAUTH_AUTH_URL, GMAIL_OAUTH_TOKEN_URL, QString(), QString(), GMAIL_OAUTH_SCOPE)) { initializeOauth(); } @@ -328,7 +328,7 @@ void GmailNetworkFactory::markMessagesStarred(RootItem::Importance importance, c void GmailNetworkFactory::onTokensError(const QString& error, const QString& error_description) { Q_UNUSED(error) - qApp->showGuiMessage(tr("Inoreader: authentication error"), + qApp->showGuiMessage(tr("Gmail: authentication error"), tr("Click this to login again. Error is: '%1'").arg(error_description), QSystemTrayIcon::Critical, nullptr, false, @@ -338,7 +338,7 @@ void GmailNetworkFactory::onTokensError(const QString& error, const QString& err } void GmailNetworkFactory::onAuthFailed() { - qApp->showGuiMessage(tr("Inoreader: authorization denied"), + qApp->showGuiMessage(tr("Gmail: authorization denied"), tr("Click this to login again."), QSystemTrayIcon::Critical, nullptr, false, diff --git a/src/services/inoreader/gui/formeditinoreaderaccount.cpp b/src/services/inoreader/gui/formeditinoreaderaccount.cpp index a7f888524..6dd4e9593 100755 --- a/src/services/inoreader/gui/formeditinoreaderaccount.cpp +++ b/src/services/inoreader/gui/formeditinoreaderaccount.cpp @@ -10,7 +10,7 @@ #include "services/inoreader/inoreaderserviceroot.h" FormEditInoreaderAccount::FormEditInoreaderAccount(QWidget* parent) : QDialog(parent), - m_oauth(new OAuth2Service(INOREADER_OAUTH_AUTH_URL, INOREADER_OAUTH_TOKEN_URL, + m_oauth(new OAuth2Service(QString(), INOREADER_OAUTH_AUTH_URL, INOREADER_OAUTH_TOKEN_URL, INOREADER_OAUTH_CLI_ID, INOREADER_OAUTH_CLI_KEY, INOREADER_OAUTH_SCOPE)), m_editableRoot(nullptr) { m_ui.setupUi(this); diff --git a/src/services/inoreader/inoreaderentrypoint.cpp b/src/services/inoreader/inoreaderentrypoint.cpp index 8359c86d7..578e8fb32 100755 --- a/src/services/inoreader/inoreaderentrypoint.cpp +++ b/src/services/inoreader/inoreaderentrypoint.cpp @@ -14,16 +14,9 @@ #include ServiceRoot* InoreaderEntryPoint::createNewRoot() const { -#if defined(USE_WEBENGINE) FormEditInoreaderAccount form_acc(qApp->mainFormWidget()); return form_acc.execForCreate(); -#else - QMessageBox::warning(qApp->mainFormWidget(), - QObject::tr("Not supported"), - QObject::tr("This plugin is not supported in NonWebEngine variant of this program.")); - return nullptr; -#endif } QList InoreaderEntryPoint::initializeSubtree() const { diff --git a/src/services/inoreader/network/inoreadernetworkfactory.cpp b/src/services/inoreader/network/inoreadernetworkfactory.cpp index 61b1e09eb..08e9d4530 100755 --- a/src/services/inoreader/network/inoreadernetworkfactory.cpp +++ b/src/services/inoreader/network/inoreadernetworkfactory.cpp @@ -24,7 +24,7 @@ InoreaderNetworkFactory::InoreaderNetworkFactory(QObject* parent) : QObject(parent), m_service(nullptr), m_username(QString()), m_batchSize(INOREADER_DEFAULT_BATCH_SIZE), - m_oauth2(new OAuth2Service(INOREADER_OAUTH_AUTH_URL, INOREADER_OAUTH_TOKEN_URL, + m_oauth2(new OAuth2Service(QString(), INOREADER_OAUTH_AUTH_URL, INOREADER_OAUTH_TOKEN_URL, INOREADER_OAUTH_CLI_ID, INOREADER_OAUTH_CLI_KEY, INOREADER_OAUTH_SCOPE)) { initializeOauth(); }