diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 167d1727f..5db93a640 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -38,6 +38,8 @@ set(client_UI_SRCS wizard/owncloudconnectionmethoddialog.ui wizard/owncloudhttpcredspage.ui wizard/owncloudoauthcredspage.ui + wizard/flow2authcredspage.ui + wizard/flow2authwidget.ui wizard/owncloudsetupnocredspage.ui wizard/owncloudwizardresultpage.ui wizard/webview.ui @@ -103,6 +105,7 @@ set(client_SRCS creds/credentialsfactory.cpp creds/httpcredentialsgui.cpp creds/oauth.cpp + creds/flow2auth.cpp creds/webflowcredentials.cpp creds/webflowcredentialsdialog.cpp wizard/postfixlineedit.cpp @@ -111,6 +114,8 @@ set(client_SRCS wizard/owncloudconnectionmethoddialog.cpp wizard/owncloudhttpcredspage.cpp wizard/owncloudoauthcredspage.cpp + wizard/flow2authcredspage.cpp + wizard/flow2authwidget.cpp wizard/owncloudsetuppage.cpp wizard/owncloudwizardcommon.cpp wizard/owncloudwizard.cpp diff --git a/src/gui/creds/flow2auth.cpp b/src/gui/creds/flow2auth.cpp new file mode 100644 index 000000000..e570e01d4 --- /dev/null +++ b/src/gui/creds/flow2auth.cpp @@ -0,0 +1,176 @@ +/* + * Copyright (C) by Olivier Goffart + * Copyright (C) by Michael Schuster + * + * 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. + */ + +#include +#include +#include +#include "account.h" +#include "creds/flow2auth.h" +#include +#include +#include "theme.h" +#include "networkjobs.h" +#include "configfile.h" + +namespace OCC { + +Q_LOGGING_CATEGORY(lcFlow2auth, "nextcloud.sync.credentials.flow2auth", QtInfoMsg) + +Flow2Auth::~Flow2Auth() +{ +} + +void Flow2Auth::start() +{ + // Note: All startup code is in openBrowser() to allow reinitiate a new request with + // fresh tokens. Opening the same pollEndpoint link twice triggers an expiration + // message by the server (security, intended design). + openBrowser(); +} + +QUrl Flow2Auth::authorisationLink() const +{ + return _loginUrl; +} + +void Flow2Auth::openBrowser() +{ + _pollTimer.stop(); + + // Step 1: Initiate a login, do an anonymous POST request + QUrl url = Utility::concatUrlPath(_account->url().toString(), QLatin1String("/index.php/login/v2")); + + auto job = _account->sendRequest("POST", url); + job->setTimeout(qMin(30 * 1000ll, job->timeoutMsec())); + + QObject::connect(job, &SimpleNetworkJob::finishedSignal, this, [this](QNetworkReply *reply) { + auto jsonData = reply->readAll(); + QJsonParseError jsonParseError; + QJsonObject json = QJsonDocument::fromJson(jsonData, &jsonParseError).object(); + + QString pollToken = json.value("poll").toObject().value("token").toString(); + QString pollEndpoint = json.value("poll").toObject().value("endpoint").toString(); + QUrl loginUrl = json["login"].toString(); + + if (reply->error() != QNetworkReply::NoError || jsonParseError.error != QJsonParseError::NoError + || json.isEmpty() || pollToken.isEmpty() || pollEndpoint.isEmpty() || loginUrl.isEmpty()) { + QString errorReason; + QString errorFromJson = json["error"].toString(); + if (!errorFromJson.isEmpty()) { + errorReason = tr("Error returned from the server: %1") + .arg(errorFromJson.toHtmlEscaped()); + } else if (reply->error() != QNetworkReply::NoError) { + errorReason = tr("There was an error accessing the 'token' endpoint:
%1") + .arg(reply->errorString().toHtmlEscaped()); + } else if (jsonParseError.error != QJsonParseError::NoError) { + errorReason = tr("Could not parse the JSON returned from the server:
%1") + .arg(jsonParseError.errorString()); + } else { + errorReason = tr("The reply from the server did not contain all expected fields"); + } + qCWarning(lcFlow2auth) << "Error when getting the loginUrl" << json << errorReason; + emit result(Error); + return; + } + + + _loginUrl = loginUrl; + _pollToken = pollToken; + _pollEndpoint = pollEndpoint; + + + // Start polling + ConfigFile cfg; + std::chrono::milliseconds polltime = cfg.remotePollInterval(); + qCInfo(lcFlow2auth) << "setting remote poll timer interval to" << polltime.count() << "msec"; + _pollTimer.setInterval(polltime.count()); + QObject::connect(&_pollTimer, &QTimer::timeout, this, &Flow2Auth::slotPollTimerTimeout); + _pollTimer.start(); + + + // Try to open Browser + if (!QDesktopServices::openUrl(authorisationLink())) { + // We cannot open the browser, then we claim we don't support Flow2Auth. + // Our UI callee should ask the user to copy and open the link. + emit result(NotSupported, QString()); + } + }); +} + +void Flow2Auth::slotPollTimerTimeout() +{ + _pollTimer.stop(); + + // Step 2: Poll + QNetworkRequest req; + req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + + auto requestBody = new QBuffer; + QUrlQuery arguments(QString("token=%1").arg(_pollToken)); + requestBody->setData(arguments.query(QUrl::FullyEncoded).toLatin1()); + + auto job = _account->sendRequest("POST", _pollEndpoint, req, requestBody); + job->setTimeout(qMin(30 * 1000ll, job->timeoutMsec())); + + QObject::connect(job, &SimpleNetworkJob::finishedSignal, this, [this](QNetworkReply *reply) { + auto jsonData = reply->readAll(); + QJsonParseError jsonParseError; + QJsonObject json = QJsonDocument::fromJson(jsonData, &jsonParseError).object(); + + QUrl serverUrl = json["server"].toString(); + QString loginName = json["loginName"].toString(); + QString appPassword = json["appPassword"].toString(); + + if (reply->error() != QNetworkReply::NoError || jsonParseError.error != QJsonParseError::NoError + || json.isEmpty() || serverUrl.isEmpty() || loginName.isEmpty() || appPassword.isEmpty()) { + QString errorReason; + QString errorFromJson = json["error"].toString(); + if (!errorFromJson.isEmpty()) { + errorReason = tr("Error returned from the server: %1") + .arg(errorFromJson.toHtmlEscaped()); + } else if (reply->error() != QNetworkReply::NoError) { + errorReason = tr("There was an error accessing the 'token' endpoint:
%1") + .arg(reply->errorString().toHtmlEscaped()); + } else if (jsonParseError.error != QJsonParseError::NoError) { + errorReason = tr("Could not parse the JSON returned from the server:
%1") + .arg(jsonParseError.errorString()); + } else { + errorReason = tr("The reply from the server did not contain all expected fields"); + } + qCDebug(lcFlow2auth) << "Error when polling for the appPassword" << json << errorReason; + + // Forget sensitive data + appPassword.clear(); + loginName.clear(); + + // Failed: poll again + _pollTimer.start(); + return; + } + + // Success + qCInfo(lcFlow2auth) << "Success getting the appPassword for user: " << loginName << ", server: " << serverUrl.toString(); + + _account->setUrl(serverUrl); + + emit result(LoggedIn, loginName, appPassword); + + // Forget sensitive data + appPassword.clear(); + loginName.clear(); + }); +} + +} // namespace OCC diff --git a/src/gui/creds/flow2auth.h b/src/gui/creds/flow2auth.h new file mode 100644 index 000000000..b53834a11 --- /dev/null +++ b/src/gui/creds/flow2auth.h @@ -0,0 +1,68 @@ +/* + * Copyright (C) by Olivier Goffart + * Copyright (C) by Michael Schuster + * + * 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. + */ + +#pragma once +#include +#include +#include +#include "accountfwd.h" + +namespace OCC { + +/** + * Job that does the authorization, grants and fetches the access token via Login Flow v2 + * + * See: https://docs.nextcloud.com/server/latest/developer_manual/client_apis/LoginFlow/index.html#login-flow-v2 + * + */ +class Flow2Auth : public QObject +{ + Q_OBJECT +public: + Flow2Auth(Account *account, QObject *parent) + : QObject(parent) + , _account(account) + { + } + ~Flow2Auth(); + + enum Result { NotSupported, + LoggedIn, + Error }; + Q_ENUM(Result); + void start(); + void openBrowser(); + QUrl authorisationLink() const; + +signals: + /** + * The state has changed. + * when logged in, appPassword has the value of the app password. + */ + void result(Flow2Auth::Result result, const QString &user = QString(), const QString &appPassword = QString()); + +private slots: + void slotPollTimerTimeout(); + +private: + Account *_account; + QUrl _loginUrl; + QString _pollToken; + QString _pollEndpoint; + QTimer _pollTimer; +}; + + +} // namespace OCC diff --git a/src/gui/creds/webflowcredentials.cpp b/src/gui/creds/webflowcredentials.cpp index 3ff15b26b..ecc0e61d9 100644 --- a/src/gui/creds/webflowcredentials.cpp +++ b/src/gui/creds/webflowcredentials.cpp @@ -102,7 +102,7 @@ bool WebFlowCredentials::ready() const { void WebFlowCredentials::fetchFromKeychain() { _wasFetched = true; - // Make sure we get the user fromt he config file + // Make sure we get the user from the config file fetchUser(); if (ready()) { @@ -114,12 +114,17 @@ void WebFlowCredentials::fetchFromKeychain() { } void WebFlowCredentials::askFromUser() { - _askDialog = new WebFlowCredentialsDialog(); + // LoginFlowV2 > WebViewFlow > OAuth > Shib > Basic + bool useFlow2 = (_account->serverVersionInt() >= Account::makeServerVersion(16, 0, 0)); - QUrl url = _account->url(); - QString path = url.path() + "/index.php/login/flow"; - url.setPath(path); - _askDialog->setUrl(url); + _askDialog = new WebFlowCredentialsDialog(_account, useFlow2); + + if (!useFlow2) { + QUrl url = _account->url(); + QString path = url.path() + "/index.php/login/flow"; + url.setPath(path); + _askDialog->setUrl(url); + } QString msg = tr("You have been logged out of %1 as user %2. Please login again") .arg(_account->displayName(), _user); @@ -142,10 +147,12 @@ void WebFlowCredentials::slotAskFromUserCredentialsProvided(const QString &user, .arg(_user); _askDialog->setError(msg); - QUrl url = _account->url(); - QString path = url.path() + "/index.php/login/flow"; - url.setPath(path); - _askDialog->setUrl(url); + if (!_askDialog->isUsingFlow2()) { + QUrl url = _account->url(); + QString path = url.path() + "/index.php/login/flow"; + url.setPath(path); + _askDialog->setUrl(url); + } return; } diff --git a/src/gui/creds/webflowcredentialsdialog.cpp b/src/gui/creds/webflowcredentialsdialog.cpp index 2d22ba06e..9971f8f34 100644 --- a/src/gui/creds/webflowcredentialsdialog.cpp +++ b/src/gui/creds/webflowcredentialsdialog.cpp @@ -3,13 +3,21 @@ #include #include +#include "theme.h" +#include "wizard/owncloudwizardcommon.h" #include "wizard/webview.h" +#include "wizard/flow2authwidget.h" namespace OCC { -WebFlowCredentialsDialog::WebFlowCredentialsDialog(QWidget *parent) - : QDialog(parent) +WebFlowCredentialsDialog::WebFlowCredentialsDialog(Account *account, bool useFlow2, QWidget *parent) + : QDialog(parent), + _useFlow2(useFlow2), + _flow2AuthWidget(nullptr), + _webView(nullptr) { + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + _layout = new QVBoxLayout(this); //QString msg = tr("You have been logged out of %1 as user %2, please login again") @@ -17,28 +25,43 @@ WebFlowCredentialsDialog::WebFlowCredentialsDialog(QWidget *parent) _infoLabel = new QLabel(); _layout->addWidget(_infoLabel); - _webView = new WebView(); - _layout->addWidget(_webView); + if (_useFlow2) { + _flow2AuthWidget = new Flow2AuthWidget(account); + _layout->addWidget(_flow2AuthWidget); + + connect(_flow2AuthWidget, &Flow2AuthWidget::urlCatched, this, &WebFlowCredentialsDialog::urlCatched); + } else { + _webView = new WebView(); + _layout->addWidget(_webView); + + connect(_webView, &WebView::urlCatched, this, &WebFlowCredentialsDialog::urlCatched); + } _errorLabel = new QLabel(); _errorLabel->hide(); _layout->addWidget(_errorLabel); - setLayout(_layout); + WizardCommon::initErrorLabel(_errorLabel); - connect(_webView, &WebView::urlCatched, this, &WebFlowCredentialsDialog::urlCatched); + setLayout(_layout); } void WebFlowCredentialsDialog::closeEvent(QCloseEvent* e) { - Q_UNUSED(e); + Q_UNUSED(e) - // Force calling WebView::~WebView() earlier so that _profile and _page are - // deleted in the correct order. - delete _webView; + if (_webView) { + // Force calling WebView::~WebView() earlier so that _profile and _page are + // deleted in the correct order. + delete _webView; + } + + if (_flow2AuthWidget) + delete _flow2AuthWidget; } void WebFlowCredentialsDialog::setUrl(const QUrl &url) { - _webView->setUrl(url); + if (_webView) + _webView->setUrl(url); } void WebFlowCredentialsDialog::setInfo(const QString &msg) { @@ -46,6 +69,11 @@ void WebFlowCredentialsDialog::setInfo(const QString &msg) { } void WebFlowCredentialsDialog::setError(const QString &error) { + if (_useFlow2 && _flow2AuthWidget) { + _flow2AuthWidget->setError(error); + return; + } + if (error.isEmpty()) { _errorLabel->hide(); } else { diff --git a/src/gui/creds/webflowcredentialsdialog.h b/src/gui/creds/webflowcredentialsdialog.h index 9849ee3a4..a540f0edb 100644 --- a/src/gui/creds/webflowcredentialsdialog.h +++ b/src/gui/creds/webflowcredentialsdialog.h @@ -4,23 +4,30 @@ #include #include +#include "accountfwd.h" + class QLabel; class QVBoxLayout; namespace OCC { class WebView; +class Flow2AuthWidget; class WebFlowCredentialsDialog : public QDialog { Q_OBJECT public: - WebFlowCredentialsDialog(QWidget *parent = nullptr); + WebFlowCredentialsDialog(Account *account, bool useFlow2, QWidget *parent = nullptr); void setUrl(const QUrl &url); void setInfo(const QString &msg); void setError(const QString &error); + bool isUsingFlow2() const { + return _useFlow2; + } + protected: void closeEvent(QCloseEvent * e) override; @@ -28,7 +35,11 @@ signals: void urlCatched(const QString user, const QString pass, const QString host); private: + bool _useFlow2; + + Flow2AuthWidget *_flow2AuthWidget; WebView *_webView; + QLabel *_errorLabel; QLabel *_infoLabel; QVBoxLayout *_layout; diff --git a/src/gui/owncloudsetupwizard.cpp b/src/gui/owncloudsetupwizard.cpp index 2a037ff84..fbe07e8b2 100644 --- a/src/gui/owncloudsetupwizard.cpp +++ b/src/gui/owncloudsetupwizard.cpp @@ -408,7 +408,7 @@ void OwncloudSetupWizard::slotAuthError() } _ocWizard->show(); - if (_ocWizard->currentId() == WizardCommon::Page_ShibbolethCreds || _ocWizard->currentId() == WizardCommon::Page_OAuthCreds) { + if (_ocWizard->currentId() == WizardCommon::Page_ShibbolethCreds || _ocWizard->currentId() == WizardCommon::Page_OAuthCreds || _ocWizard->currentId() == WizardCommon::Page_Flow2AuthCreds) { _ocWizard->back(); } _ocWizard->displayError(errorMsg, _ocWizard->currentId() == WizardCommon::Page_ServerSetup && checkDowngradeAdvised(reply)); diff --git a/src/gui/wizard/flow2authcredspage.cpp b/src/gui/wizard/flow2authcredspage.cpp new file mode 100644 index 000000000..b52095f0d --- /dev/null +++ b/src/gui/wizard/flow2authcredspage.cpp @@ -0,0 +1,148 @@ +/* + * Copyright (C) by Olivier Goffart + * Copyright (C) by Michael Schuster + * + * 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. + */ + +#include +#include +#include + +#include "wizard/flow2authcredspage.h" +#include "theme.h" +#include "account.h" +#include "cookiejar.h" +#include "wizard/owncloudwizardcommon.h" +#include "wizard/owncloudwizard.h" +#include "creds/credentialsfactory.h" +#include "creds/webflowcredentials.h" + +namespace OCC { + +Flow2AuthCredsPage::Flow2AuthCredsPage() + : AbstractCredentialsWizardPage() +{ + _ui.setupUi(this); + + Theme *theme = Theme::instance(); + _ui.topLabel->hide(); + _ui.bottomLabel->hide(); + QVariant variant = theme->customMedia(Theme::oCSetupTop); + WizardCommon::setupCustomMedia(variant, _ui.topLabel); + variant = theme->customMedia(Theme::oCSetupBottom); + WizardCommon::setupCustomMedia(variant, _ui.bottomLabel); + + WizardCommon::initErrorLabel(_ui.errorLabel); + + setTitle(WizardCommon::titleTemplate().arg(tr("Connect to %1").arg(Theme::instance()->appNameGUI()))); + setSubTitle(WizardCommon::subTitleTemplate().arg(tr("Login in your browser (Login Flow v2)"))); + + connect(_ui.openLinkButton, &QCommandLinkButton::clicked, [this] { + _ui.errorLabel->hide(); + if (_asyncAuth) + _asyncAuth->openBrowser(); + }); + _ui.openLinkButton->setContextMenuPolicy(Qt::CustomContextMenu); + QObject::connect(_ui.openLinkButton, &QWidget::customContextMenuRequested, [this](const QPoint &pos) { + auto menu = new QMenu(_ui.openLinkButton); + menu->addAction(tr("Copy link to clipboard"), this, [this] { + if (_asyncAuth) + QApplication::clipboard()->setText(_asyncAuth->authorisationLink().toString(QUrl::FullyEncoded)); + }); + menu->setAttribute(Qt::WA_DeleteOnClose); + menu->popup(_ui.openLinkButton->mapToGlobal(pos)); + }); +} + +void Flow2AuthCredsPage::initializePage() +{ + OwncloudWizard *ocWizard = qobject_cast(wizard()); + Q_ASSERT(ocWizard); + ocWizard->account()->setCredentials(CredentialsFactory::create("http")); + _asyncAuth.reset(new Flow2Auth(ocWizard->account().data(), this)); + connect(_asyncAuth.data(), &Flow2Auth::result, this, &Flow2AuthCredsPage::asyncAuthResult, Qt::QueuedConnection); + _asyncAuth->start(); + + // Don't hide the wizard (avoid user confusion)! + //wizard()->hide(); +} + +void OCC::Flow2AuthCredsPage::cleanupPage() +{ + // The next or back button was activated, show the wizard again + wizard()->show(); + _asyncAuth.reset(); + + // Forget sensitive data + _appPassword.clear(); + _user.clear(); +} + +void Flow2AuthCredsPage::asyncAuthResult(Flow2Auth::Result r, const QString &user, + const QString &appPassword) +{ + switch (r) { + case Flow2Auth::NotSupported: { + /* Flow2Auth not supported (can't open browser) */ + _ui.errorLabel->setText(tr("Unable to open the Browser, please copy the link to your Browser.")); + _ui.errorLabel->show(); + + /* Don't fallback to HTTP credentials */ + /*OwncloudWizard *ocWizard = qobject_cast(wizard()); + ocWizard->back(); + ocWizard->setAuthType(DetermineAuthTypeJob::Basic);*/ + break; + } + case Flow2Auth::Error: + /* Error while getting the access token. (Timeout, or the server did not accept our client credentials */ + _ui.errorLabel->show(); + wizard()->show(); + break; + case Flow2Auth::LoggedIn: { + _user = user; + _appPassword = appPassword; + OwncloudWizard *ocWizard = qobject_cast(wizard()); + Q_ASSERT(ocWizard); + emit connectToOCUrl(ocWizard->account()->url().toString()); + break; + } + } +} + +int Flow2AuthCredsPage::nextId() const +{ + return WizardCommon::Page_AdvancedSetup; +} + +void Flow2AuthCredsPage::setConnected() +{ + wizard()->show(); +} + +AbstractCredentials *Flow2AuthCredsPage::getCredentials() const +{ + OwncloudWizard *ocWizard = qobject_cast(wizard()); + Q_ASSERT(ocWizard); + return new WebFlowCredentials( + _user, + _appPassword, + ocWizard->_clientSslCertificate, + ocWizard->_clientSslKey + ); +} + +bool Flow2AuthCredsPage::isComplete() const +{ + return false; /* We can never go forward manually */ +} + +} // namespace OCC diff --git a/src/gui/wizard/flow2authcredspage.h b/src/gui/wizard/flow2authcredspage.h new file mode 100644 index 000000000..bffcf68b3 --- /dev/null +++ b/src/gui/wizard/flow2authcredspage.h @@ -0,0 +1,61 @@ +/* + * Copyright (C) by Olivier Goffart + * Copyright (C) by Michael Schuster + * + * 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. + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include "wizard/abstractcredswizardpage.h" +#include "accountfwd.h" +#include "creds/flow2auth.h" + +#include "ui_flow2authcredspage.h" + + +namespace OCC { + + +class Flow2AuthCredsPage : public AbstractCredentialsWizardPage +{ + Q_OBJECT +public: + Flow2AuthCredsPage(); + + AbstractCredentials *getCredentials() const override; + + void initializePage() override; + void cleanupPage() override; + int nextId() const override; + void setConnected(); + bool isComplete() const override; + +public Q_SLOTS: + void asyncAuthResult(Flow2Auth::Result, const QString &user, const QString &appPassword); + +signals: + void connectToOCUrl(const QString &); + +public: + QString _user; + QString _appPassword; + QScopedPointer _asyncAuth; + Ui_Flow2AuthCredsPage _ui; +}; + +} // namespace OCC diff --git a/src/gui/wizard/flow2authcredspage.ui b/src/gui/wizard/flow2authcredspage.ui new file mode 100644 index 000000000..04c1d7217 --- /dev/null +++ b/src/gui/wizard/flow2authcredspage.ui @@ -0,0 +1,87 @@ + + + Flow2AuthCredsPage + + + + 0 + 0 + 424 + 373 + + + + Form + + + + + + TextLabel + + + Qt::RichText + + + Qt::AlignCenter + + + true + + + + + + + Please switch to your browser to proceed. + + + true + + + + + + + An error occurred while connecting. Please try again. + + + Qt::PlainText + + + + + + + Re-open Browser (or right-click to copy link) + + + + + + + Qt::Vertical + + + + 20 + 127 + + + + + + + + TextLabel + + + Qt::RichText + + + + + + + + diff --git a/src/gui/wizard/flow2authwidget.cpp b/src/gui/wizard/flow2authwidget.cpp new file mode 100644 index 000000000..300b2e83d --- /dev/null +++ b/src/gui/wizard/flow2authwidget.cpp @@ -0,0 +1,113 @@ +/* + * Copyright (C) by Michael Schuster + * + * 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. + */ + +#include "flow2authwidget.h" + +#include +#include +#include +#include +#include + +#include +#include + +#include "common/utility.h" +#include "account.h" +#include "theme.h" +#include "wizard/owncloudwizardcommon.h" + +namespace OCC { + +Q_LOGGING_CATEGORY(lcFlow2AuthWidget, "gui.wizard.flow2authwidget", QtInfoMsg) + + +Flow2AuthWidget::Flow2AuthWidget(Account *account, QWidget *parent) + : QWidget(parent), + _account(account), + _ui() +{ + _ui.setupUi(this); + + Theme *theme = Theme::instance(); + _ui.topLabel->hide(); + _ui.bottomLabel->hide(); + QVariant variant = theme->customMedia(Theme::oCSetupTop); + WizardCommon::setupCustomMedia(variant, _ui.topLabel); + variant = theme->customMedia(Theme::oCSetupBottom); + WizardCommon::setupCustomMedia(variant, _ui.bottomLabel); + + WizardCommon::initErrorLabel(_ui.errorLabel); + + connect(_ui.openLinkButton, &QCommandLinkButton::clicked, [this] { + _ui.errorLabel->hide(); + if (_asyncAuth) + _asyncAuth->openBrowser(); + }); + _ui.openLinkButton->setContextMenuPolicy(Qt::CustomContextMenu); + QObject::connect(_ui.openLinkButton, &QWidget::customContextMenuRequested, [this](const QPoint &pos) { + auto menu = new QMenu(_ui.openLinkButton); + menu->addAction(tr("Copy link to clipboard"), this, [this] { + if (_asyncAuth) + QApplication::clipboard()->setText(_asyncAuth->authorisationLink().toString(QUrl::FullyEncoded)); + }); + menu->setAttribute(Qt::WA_DeleteOnClose); + menu->popup(_ui.openLinkButton->mapToGlobal(pos)); + }); + + _asyncAuth.reset(new Flow2Auth(_account, this)); + connect(_asyncAuth.data(), &Flow2Auth::result, this, &Flow2AuthWidget::asyncAuthResult, Qt::QueuedConnection); + _asyncAuth->start(); +} + +void Flow2AuthWidget::asyncAuthResult(Flow2Auth::Result r, const QString &user, + const QString &appPassword) +{ + switch (r) { + case Flow2Auth::NotSupported: + /* Flow2Auth can't open browser */ + _ui.errorLabel->setText(tr("Unable to open the Browser, please copy the link to your Browser.")); + _ui.errorLabel->show(); + break; + case Flow2Auth::Error: + /* Error while getting the access token. (Timeout, or the server did not accept our client credentials */ + _ui.errorLabel->show(); + break; + case Flow2Auth::LoggedIn: { + _user = user; + _appPassword = appPassword; + emit urlCatched(_user, _appPassword, QString()); + break; + } + } +} + +void Flow2AuthWidget::setError(const QString &error) { + if (error.isEmpty()) { + _ui.errorLabel->hide(); + } else { + _ui.errorLabel->setText(error); + _ui.errorLabel->show(); + } +} + +Flow2AuthWidget::~Flow2AuthWidget() { + _asyncAuth.reset(); + + // Forget sensitive data + _appPassword.clear(); + _user.clear(); +} + +} diff --git a/src/gui/wizard/flow2authwidget.h b/src/gui/wizard/flow2authwidget.h new file mode 100644 index 000000000..cf04d9193 --- /dev/null +++ b/src/gui/wizard/flow2authwidget.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) by Michael Schuster + * + * 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. + */ + +#ifndef FLOW2AUTHWIDGET_H +#define FLOW2AUTHWIDGET_H + +#include +#include + +#include "creds/flow2auth.h" + +#include "ui_flow2authwidget.h" + +namespace OCC { + +class Flow2AuthWidget : public QWidget +{ + Q_OBJECT +public: + Flow2AuthWidget(Account *account, QWidget *parent = nullptr); + virtual ~Flow2AuthWidget(); + + void setError(const QString &error); + +public Q_SLOTS: + void asyncAuthResult(Flow2Auth::Result, const QString &user, const QString &appPassword); + +signals: + void urlCatched(const QString user, const QString pass, const QString host); + +private: + Account *_account; + QString _user; + QString _appPassword; + QScopedPointer _asyncAuth; + Ui_Flow2AuthWidget _ui; +}; + +} + +#endif // FLOW2AUTHWIDGET_H diff --git a/src/gui/wizard/flow2authwidget.ui b/src/gui/wizard/flow2authwidget.ui new file mode 100644 index 000000000..e73ae6a1d --- /dev/null +++ b/src/gui/wizard/flow2authwidget.ui @@ -0,0 +1,99 @@ + + + Flow2AuthWidget + + + + 0 + 0 + 500 + 280 + + + + + 0 + 0 + + + + + 500 + 280 + + + + Form + + + + + + TextLabel + + + Qt::RichText + + + Qt::AlignCenter + + + true + + + + + + + Please switch to your browser to proceed. + + + true + + + + + + + An error occurred while connecting. Please try again. + + + Qt::PlainText + + + + + + + Re-open Browser (or right-click to copy link) + + + + + + + Qt::Vertical + + + + 20 + 127 + + + + + + + + TextLabel + + + Qt::RichText + + + + + + + + diff --git a/src/gui/wizard/owncloudoauthcredspage.cpp b/src/gui/wizard/owncloudoauthcredspage.cpp index a32ccdb87..174aa05c4 100644 --- a/src/gui/wizard/owncloudoauthcredspage.cpp +++ b/src/gui/wizard/owncloudoauthcredspage.cpp @@ -70,7 +70,9 @@ void OwncloudOAuthCredsPage::initializePage() _asyncAuth.reset(new OAuth(ocWizard->account().data(), this)); connect(_asyncAuth.data(), &OAuth::result, this, &OwncloudOAuthCredsPage::asyncAuthResult, Qt::QueuedConnection); _asyncAuth->start(); - wizard()->hide(); + + // Don't hide the wizard (avoid user confusion)! + //wizard()->hide(); } void OCC::OwncloudOAuthCredsPage::cleanupPage() diff --git a/src/gui/wizard/owncloudsetuppage.cpp b/src/gui/wizard/owncloudsetuppage.cpp index 4af0ca935..8cfa158d4 100644 --- a/src/gui/wizard/owncloudsetuppage.cpp +++ b/src/gui/wizard/owncloudsetuppage.cpp @@ -258,6 +258,8 @@ int OwncloudSetupPage::nextId() const return WizardCommon::Page_HttpCreds; case DetermineAuthTypeJob::OAuth: return WizardCommon::Page_OAuthCreds; + case DetermineAuthTypeJob::LoginFlowV2: + return WizardCommon::Page_Flow2AuthCreds; case DetermineAuthTypeJob::Shibboleth: return WizardCommon::Page_ShibbolethCreds; case DetermineAuthTypeJob::WebViewFlow: diff --git a/src/gui/wizard/owncloudwizard.cpp b/src/gui/wizard/owncloudwizard.cpp index 357b4e005..e4dda0fbd 100644 --- a/src/gui/wizard/owncloudwizard.cpp +++ b/src/gui/wizard/owncloudwizard.cpp @@ -27,6 +27,7 @@ #include "wizard/owncloudadvancedsetuppage.h" #include "wizard/owncloudwizardresultpage.h" #include "wizard/webviewpage.h" +#include "wizard/flow2authcredspage.h" #include "QProgressIndicator.h" @@ -45,6 +46,7 @@ OwncloudWizard::OwncloudWizard(QWidget *parent) , _setupPage(new OwncloudSetupPage(this)) , _httpCredsPage(new OwncloudHttpCredsPage(this)) , _browserCredsPage(new OwncloudOAuthCredsPage) + , _flow2CredsPage(new Flow2AuthCredsPage) #ifndef NO_SHIBBOLETH , _shibbolethCredsPage(new OwncloudShibbolethCredsPage) #endif @@ -59,6 +61,7 @@ OwncloudWizard::OwncloudWizard(QWidget *parent) setPage(WizardCommon::Page_ServerSetup, _setupPage); setPage(WizardCommon::Page_HttpCreds, _httpCredsPage); setPage(WizardCommon::Page_OAuthCreds, _browserCredsPage); + setPage(WizardCommon::Page_Flow2AuthCreds, _flow2CredsPage); #ifndef NO_SHIBBOLETH setPage(WizardCommon::Page_ShibbolethCreds, _shibbolethCredsPage); #endif @@ -76,6 +79,7 @@ OwncloudWizard::OwncloudWizard(QWidget *parent) connect(_setupPage, &OwncloudSetupPage::determineAuthType, this, &OwncloudWizard::determineAuthType); connect(_httpCredsPage, &OwncloudHttpCredsPage::connectToOCUrl, this, &OwncloudWizard::connectToOCUrl); connect(_browserCredsPage, &OwncloudOAuthCredsPage::connectToOCUrl, this, &OwncloudWizard::connectToOCUrl); + connect(_flow2CredsPage, &Flow2AuthCredsPage::connectToOCUrl, this, &OwncloudWizard::connectToOCUrl); #ifndef NO_SHIBBOLETH connect(_shibbolethCredsPage, &OwncloudShibbolethCredsPage::connectToOCUrl, this, &OwncloudWizard::connectToOCUrl); #endif @@ -129,11 +133,13 @@ QString OwncloudWizard::ocUrl() const return url; } -bool OwncloudWizard::registration() { +bool OwncloudWizard::registration() +{ return _registration; } -void OwncloudWizard::setRegistration(bool registration) { +void OwncloudWizard::setRegistration(bool registration) +{ _registration = registration; } @@ -162,6 +168,10 @@ void OwncloudWizard::successfulStep() _browserCredsPage->setConnected(); break; + case WizardCommon::Page_Flow2AuthCreds: + _flow2CredsPage->setConnected(); + break; + #ifndef NO_SHIBBOLETH case WizardCommon::Page_ShibbolethCreds: _shibbolethCredsPage->setConnected(); @@ -195,6 +205,8 @@ void OwncloudWizard::setAuthType(DetermineAuthTypeJob::AuthType type) #endif if (type == DetermineAuthTypeJob::OAuth) { _credentialsPage = _browserCredsPage; + } else if (type == DetermineAuthTypeJob::LoginFlowV2) { + _credentialsPage = _flow2CredsPage; } else if (type == DetermineAuthTypeJob::WebViewFlow) { _credentialsPage = _webViewPage; } else { // try Basic auth even for "Unknown" @@ -221,7 +233,7 @@ void OwncloudWizard::slotCurrentPageChanged(int id) } setOption(QWizard::HaveCustomButton1, id == WizardCommon::Page_AdvancedSetup); - if (id == WizardCommon::Page_AdvancedSetup && _credentialsPage == _browserCredsPage) { + if (id == WizardCommon::Page_AdvancedSetup && (_credentialsPage == _browserCredsPage || _credentialsPage == _flow2CredsPage)) { // For OAuth, disable the back button in the Page_AdvancedSetup because we don't want // to re-open the browser. button(QWizard::BackButton)->setEnabled(false); diff --git a/src/gui/wizard/owncloudwizard.h b/src/gui/wizard/owncloudwizard.h index 1429709f1..c37bfd97e 100644 --- a/src/gui/wizard/owncloudwizard.h +++ b/src/gui/wizard/owncloudwizard.h @@ -40,6 +40,7 @@ class OwncloudWizardResultPage; class AbstractCredentials; class AbstractCredentialsWizardPage; class WebViewPage; +class Flow2AuthCredsPage; /** * @brief The OwncloudWizard class @@ -103,6 +104,7 @@ private: #ifndef NO_SHIBBOLETH OwncloudShibbolethCredsPage *_shibbolethCredsPage; #endif + Flow2AuthCredsPage *_flow2CredsPage; OwncloudAdvancedSetupPage *_advancedSetupPage; OwncloudWizardResultPage *_resultPage; AbstractCredentialsWizardPage *_credentialsPage; diff --git a/src/gui/wizard/owncloudwizardcommon.h b/src/gui/wizard/owncloudwizardcommon.h index c3174d15a..d1f7c08be 100644 --- a/src/gui/wizard/owncloudwizardcommon.h +++ b/src/gui/wizard/owncloudwizardcommon.h @@ -38,6 +38,7 @@ namespace WizardCommon { Page_HttpCreds, Page_ShibbolethCreds, Page_OAuthCreds, + Page_Flow2AuthCreds, Page_WebView, Page_AdvancedSetup, Page_Result diff --git a/src/libsync/creds/httpcredentials.cpp b/src/libsync/creds/httpcredentials.cpp index 3090bdb30..fbe01331e 100644 --- a/src/libsync/creds/httpcredentials.cpp +++ b/src/libsync/creds/httpcredentials.cpp @@ -173,7 +173,7 @@ void HttpCredentials::fetchFromKeychain() fetchUser(); if (!_ready && !_refreshToken.isEmpty()) { - // This happens if the credentials are still loaded from the keychain, bur we are called + // This happens if the credentials are still loaded from the keychain, but we are called // here because the auth is invalid, so this means we simply need to refresh the credentials refreshAccessToken(); return; diff --git a/src/libsync/networkjobs.cpp b/src/libsync/networkjobs.cpp index cf59b5ab6..c17889519 100644 --- a/src/libsync/networkjobs.cpp +++ b/src/libsync/networkjobs.cpp @@ -925,6 +925,11 @@ void DetermineAuthTypeJob::checkBothDone() result = WebViewFlow; } + // LoginFlowV2 > WebViewFlow > OAuth > Shib > Basic + if (_account->serverVersionInt() >= Account::makeServerVersion(16, 0, 0)) { + result = LoginFlowV2; + } + qCInfo(lcDetermineAuthTypeJob) << "Auth type for" << _account->davUrl() << "is" << result; emit authType(result); deleteLater(); diff --git a/src/libsync/networkjobs.h b/src/libsync/networkjobs.h index c2fefd611..d0829b463 100644 --- a/src/libsync/networkjobs.h +++ b/src/libsync/networkjobs.h @@ -412,7 +412,8 @@ public: Basic, // also the catch-all fallback for backwards compatibility reasons OAuth, Shibboleth, - WebViewFlow + WebViewFlow, + LoginFlowV2 }; explicit DetermineAuthTypeJob(AccountPtr account, QObject *parent = nullptr);