From f9f71436818a5c2e18eab6de9117c6a611eb6ecc Mon Sep 17 00:00:00 2001 From: Egor Nemtsev Date: Fri, 6 Sep 2019 19:27:23 +0300 Subject: Authorization rework - move widget-dependent web page interaction to qml part,so this doesn't require special build for appman - add captcha processing - use Neptune-style controls in AuthView Task-number: AUTOSUITE-1197 Change-Id: If52552eee29abc370b0aefe5ceb1edb11e9b024c Reviewed-by: Bramastyo Harimukti Santoso --- app/AlexaView.qml | 12 +- app/AuthView.qml | 261 ++++++++++++++------------- app/AuthWebPageInteraction.qml | 256 ++++++++++++++++++++++++++ app/Header.qml | 7 +- app/MainView.qml | 22 ++- app/app.pro | 3 +- plugins/alexaauth/alexaauth.cpp | 317 +++++++++------------------------ plugins/alexaauth/alexaauth.h | 130 ++++++-------- plugins/alexaauth/alexaauth.pro | 16 +- plugins/alexaauth/alexaauth_plugin.cpp | 2 +- 10 files changed, 561 insertions(+), 465 deletions(-) create mode 100644 app/AuthWebPageInteraction.qml diff --git a/app/AlexaView.qml b/app/AlexaView.qml index cae6809..7e55d12 100644 --- a/app/AlexaView.qml +++ b/app/AlexaView.qml @@ -65,16 +65,17 @@ Control { target: interactionButton height: Sizes.dp(120) anchors.horizontalCenterOffset: -root.width/2 + interactionButton.width / 2 + Sizes.dp(50) - anchors.topMargin: Sizes.dp(550) - interactionButton.height / 2 + anchors.topMargin: Sizes.dp(100) - interactionButton.height / 2 } PropertyChanges { target: stopSpeakingButton anchors.horizontalCenterOffset: root.width/2 - stopSpeakingButton.width / 2 - Sizes.dp(50) - anchors.topMargin: Sizes.dp(550) - stopSpeakingButton.height / 2 + anchors.topMargin: Sizes.dp(100) - stopSpeakingButton.height / 2 } PropertyChanges { target: cardPane - height: root.height / 2 + anchors.topMargin: interactionButton.height * 1.5 + height: root.height - cardPane.anchors.topMargin opacity: 1 } PropertyChanges { @@ -265,7 +266,7 @@ Control { visible: opacity > 0 anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top - anchors.topMargin: Sizes.dp(600) + anchors.topMargin: Sizes.dp(300) width: height height: parent.height/2 > Sizes.dp(270) ? Sizes.dp(270) : parent.height/2 background: Rectangle { @@ -319,7 +320,7 @@ Control { id: stopSpeakingButton anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top - anchors.topMargin: Sizes.dp(600) + interactionButton.height + Sizes.dp(100) + anchors.topMargin: interactionButton.anchors.topMargin + interactionButton.height + Sizes.dp(100) width: parent.width > Sizes.dp(270) ? Sizes.dp(270) : parent.width height: Sizes.dp(70) opacity: (AlexaInterface.dialogState === Alexa.Listening) || (AlexaInterface.dialogState === Alexa.Speaking) ? 1 : 0 @@ -338,7 +339,6 @@ Control { Item { id: cardPane anchors.top: parent.top - anchors.topMargin: Sizes.dp(650) anchors.left: parent.left anchors.right: parent.right height: 0 diff --git a/app/AuthView.qml b/app/AuthView.qml index e34f35f..b787d4a 100644 --- a/app/AuthView.qml +++ b/app/AuthView.qml @@ -32,11 +32,10 @@ import QtQuick 2.12 import QtQuick.Controls 2.5 import QtGraphicalEffects 1.0 - +import QtQuick.Layouts 1.13 import QtWebView 1.1 import alexainterface 1.0 -import alexaauth 1.0 import shared.utils 1.0 import shared.controls 1.0 @@ -49,52 +48,41 @@ Control { property var alexaAuth property bool authorizationRequested: false - Item { + ColumnLayout { id: initStateView anchors.top: parent.top - anchors.topMargin: Sizes.dp(500) - width: 0.5 * root.width + anchors.topMargin: Sizes.dp(200) + width: 0.4 * root.width anchors.horizontalCenter: parent.horizontalCenter opacity: 0 visible: opacity > 0 + spacing: Sizes.dp(25) - DropShadow { - anchors.fill: emailField - horizontalOffset: Sizes.dp(1) - verticalOffset: Sizes.dp(2) - radius: 6 - color: "#80000000" - source: emailField.background + Label { + id: loginLabel + text: qsTr("Amazon account:") + width: parent.width + font.pixelSize: Sizes.fontSizeM + Layout.alignment: Qt.AlignHCenter } TextField { id: emailField - width: parent.width - height: Sizes.dp(54) color: "gray" font.pixelSize: Sizes.fontSizeS placeholderText: "email" + inputMethodHints: Qt.ImhEmailCharactersOnly background: Rectangle { anchors.fill: parent - radius: 4 + radius: Sizes.dp(4) } - } - - DropShadow { - anchors.fill: passwordField - horizontalOffset: Sizes.dp(1) - verticalOffset: Sizes.dp(2) - radius: 6 - color: "#80000000" - source: passwordField.background + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true } TextField { id: passwordField - width: parent.width height: Sizes.dp(54) - anchors.top: emailField.bottom - anchors.topMargin: Sizes.dp(25) color: "gray" font.pixelSize: Sizes.fontSizeS placeholderText: "password" @@ -102,154 +90,159 @@ Control { passwordCharacter: '*' background: Rectangle { anchors.fill: parent - radius: 4 + radius: Sizes.dp(4) } + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true } } - Item { + ColumnLayout { + id: captchaView + anchors.top: parent.top + anchors.topMargin: Sizes.dp(200) + width: 0.5 * root.width + anchors.horizontalCenter: parent.horizontalCenter + opacity: 0 + visible: opacity > 0 + spacing: Sizes.dp(25) + + Image { + id: captchaImage + width: Sizes.dp(sourceSize.width) + height: Sizes.dp(sourceSize.height) + source: alexaAuth.captchaUrl + onSourceChanged: { + captchaField.text = "" + } + Layout.alignment: Qt.AlignHCenter + } + + TextField { + id: captchaField + width: parent.width + color: "gray" + text: "" + placeholderText: "Enter the characters you see" + font.pixelSize: Sizes.fontSizeS + background: Rectangle { + anchors.fill: parent + radius: Sizes.dp(4) + } + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true + } + } + + ColumnLayout { id: manualAuthorization anchors.horizontalCenter: parent.horizontalCenter width: 0.65 * parent.width opacity: 0 visible: opacity > 0 + anchors.top: parent.top + anchors.bottom: parent.bottom + spacing: Sizes.dp(15) Label { id: authCode - anchors.bottom: webView.top - anchors.horizontalCenter: webView.horizontalCenter - height: Sizes.dp(60) - verticalAlignment: Text.AlignVCenter font.pixelSize: Sizes.fontSizeM text: "Your code: " + AlexaInterface.authCode visible: AlexaInterface.authCode !== "" + Layout.alignment: Qt.AlignHCenter + } + + Label { + id: errorText + width: parent.width + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.Wrap + font.pixelSize: Sizes.fontSizeS + text: "Cannot authorize due to invalid client id. Check the client id in the AlexaClientSDKConfig.json and restart the Alexa application" + visible: webView.url.toString() === "" + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true } WebView { id: webView - y: Sizes.dp(436) + Sizes.dp(100) - width: parent.width height: Sizes.dp(700) - anchors.horizontalCenter: parent.horizontalCenter url: AlexaInterface.authUrl visible: url !== "" + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true } Label { id: helpText - anchors.top: webView.bottom - anchors.topMargin: Sizes.dp(15) - anchors.horizontalCenter: parent.horizontalCenter - width: parent.width horizontalAlignment: Text.AlignHCenter wrapMode: Text.Wrap font.pixelSize: Sizes.fontSizeS text: "Couldn't authorize automatically. Proceed by filling your email, password and authorization code in the web form." visible: webView.url.toString() !== "" - } - - Label { - id: errorText - anchors.centerIn: parent - anchors.verticalCenterOffset: Sizes.dp(600) - width: parent.width - horizontalAlignment: Text.AlignHCenter - wrapMode: Text.Wrap - font.pixelSize: Sizes.fontSizeS - text: "Cannot authorize due to invalid client id. Check the client id in the AlexaClientSDKConfig.json and restart the Alexa application" - visible: webView.url.toString() === "" + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true } } - // The item holds the same position than the interaction button in AlexaView - // The purpose is to prevent a jump when moved from AuthView to AlexaView - Item { - id: authButtonItem + + Button { + id: authButton + + implicitWidth: Sizes.dp(315) + implicitHeight: Sizes.dp(64) anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top - anchors.topMargin: Sizes.dp(600) - height: parent.height/2 > Sizes.dp(270) ? Sizes.dp(270) : parent.height/2 - width: height + anchors.topMargin: Sizes.dp(500) visible: opacity > 0 opacity: 1 - - DropShadow { - anchors.fill: authButton - horizontalOffset: Sizes.dp(3) - verticalOffset: Sizes.dp(3) - radius: 8.0 - samples: 17 - color: "#80000000" - source: authButton.background - } - - Button { - id: authButton - anchors.centerIn: parent - width: parent.width - height: Sizes.dp(70) - enabled: !alexaAuth.isAuthorizing && emailField.text !== "" && passwordField.text !== "" - text: "Authorize" - font.pixelSize: Sizes.fontSizeL - icon.color: enabled ? Style.contrastColor : "black" - background: ButtonBackground { - border.color: parent.enabled ? "#5FCAF4" : "lightgray" - color: parent.enabled ? "#5FCAF4" : "lightgray" - - } - onClicked: { - alexaAuth.email = emailField.text - alexaAuth.password = passwordField.text + enabled: (!alexaAuth.isAuthorizing && emailField.text !== "" && passwordField.text !== "") + || (alexaAuth.isFillingCaptcha && captchaField.text !== "") + text: qsTr("Authorize") + font.pixelSize: Sizes.fontSizeS + onClicked: { + alexaAuth.email = emailField.text + alexaAuth.password = passwordField.text + if (alexaAuth.isFillingCaptcha) { + alexaAuth.captcha = captchaField.text + alexaAuth.authorizeWithCaptcha() + } else { alexaAuth.authorize() - root.authorizationRequested = true } + root.authorizationRequested = true } } - Row { - anchors.top: authButtonItem.bottom - anchors.topMargin: Sizes.dp(40) - anchors.horizontalCenter: parent.horizontalCenter - spacing: Sizes.dp(20) + ColumnLayout { + anchors.top: parent.top + width: parent.width - Item { - id: spinner - width: Sizes.dp(50) - height: width - anchors.verticalCenter: parent.verticalCenter + ProgressBar { + id: progress + width: parent.width + implicitHeight: Sizes.dp(8) opacity: 0 visible: opacity > 0 - - Image { - id: spinnerImage - anchors.fill: parent - fillMode: Image.PreserveAspectFit - source: "assets/spinner.png" - } - - ColorOverlay { - id: overlay - anchors.fill: spinnerImage - source: spinnerImage - color: "#63abc8" - visible: spinnerImage.opacity > 0 - RotationAnimation on rotation { - loops: Animation.Infinite - from: 0 - to: 360 - duration: 1000 - running: overlay.visible - } + from: 0 + to: 1 + value: 0 + indeterminate: true + Layout.fillWidth: true + SequentialAnimation on value { + loops: Animation.Infinite + PropertyAnimation { to: 0; duration: 1500 } + PropertyAnimation { to: 1; duration: 1500 } } } Label { id: authAppText - anchors.verticalCenter: parent.verticalCenter - text: qsTr("Authorizing application...") + text: qsTr("Authorizing device...") font.pixelSize: Sizes.fontSizeL opacity: 0 visible: opacity > 0 + Layout.topMargin: Sizes.dp(400) + Layout.alignment: Qt.AlignHCenter } } @@ -258,26 +251,36 @@ Control { name: "initial_state" PropertyChanges { target: initStateView; opacity: 1 } }, + State { + name: "captcha" + PropertyChanges { target: captchaView; opacity: 1; anchors.top: initStateView.bottom; + anchors.topMargin: Sizes.dp(25) } + PropertyChanges { target: initStateView; opacity: 1 } + PropertyChanges { target: progress; opacity: 0 } + PropertyChanges { target: authAppText; opacity: 0 } + PropertyChanges { target: authButton; opacity: 1; anchors.top: captchaView.bottom; + anchors.topMargin: Sizes.dp(25) } + }, State { name: "automatic_auth" PropertyChanges { target: initStateView; opacity: 0 } PropertyChanges { target: authAppText; opacity: 0.8 } - PropertyChanges { target: spinner; opacity: 1 } + PropertyChanges { target: progress; opacity: 1 } }, State { name: "manual_auth" PropertyChanges { target: initStateView; opacity: 0 } PropertyChanges { target: manualAuthorization; opacity: 1 } - PropertyChanges { target: spinner; opacity: 0 } - PropertyChanges { target: authButtonItem; opacity: 0 } + PropertyChanges { target: progress; opacity: 0 } + PropertyChanges { target: authButton; opacity: 0 } PropertyChanges { target: authAppText; opacity: 0 } }, State { name: "complete_auth" PropertyChanges { target: manualAuthorization; opacity: 0 } PropertyChanges { target: authAppText; opacity: 0.8; text: "Authorization completed" } - PropertyChanges { target: spinner; opacity: 0 } - PropertyChanges { target: authButtonItem; opacity: 0 } + PropertyChanges { target: progress; opacity: 0 } + PropertyChanges { target: authButton; opacity: 0 } } ] @@ -286,7 +289,11 @@ Control { if (!root.authorizationRequested) { return "initial_state" } else if (alexaAuth.isAuthorizing) { - return "automatic_auth" + if (alexaAuth.isFillingCaptcha) { + return "captcha" + } else { + return "automatic_auth" + } } else { return "manual_auth" } diff --git a/app/AuthWebPageInteraction.qml b/app/AuthWebPageInteraction.qml new file mode 100644 index 0000000..6d133b4 --- /dev/null +++ b/app/AuthWebPageInteraction.qml @@ -0,0 +1,256 @@ +/**************************************************************************** +** +** Copyright (C) 2019 Luxoft Sweden AB +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Neptune 3 UI. +** +** $QT_BEGIN_LICENSE:GPL-QTAS$ +** Commercial License Usage +** Licensees holding valid commercial Qt Automotive Suite licenses may use +** this file in accordance with the commercial license agreement provided +** with the Software or, alternatively, in accordance with the terms +** contained in a written agreement between you and The Qt Company. For +** licensing terms and conditions see https://www.qt.io/terms-conditions. +** For further information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 or (at your option) any later version +** approved by the KDE Free Qt Foundation. The licenses are as published by +** the Free Software Foundation and appearing in the file LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +** SPDX-License-Identifier: GPL-3.0 +** +****************************************************************************/ + +import QtQuick 2.0 +import QtWebEngine 1.8 + +import alexaauth 1.0 + +QtObject { + id: root + + property bool isAuthorizing: false + property string authCode: "" + property url authUrl: "" + property int error: AlexaAuth.NoError + property string httpUserAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36" + property bool authorizationSucceed: false + property string email: "" + property string password: "" + property bool isFillingCaptcha: false + property url captchaUrl: "" + property string captcha: "" + property WebEngineView authPage: WebEngineView { + id: authPage + + property int retryCount: 0 + + function callAuthAction() { + switch (AlexaAuth.getAuthStage(authPage.title)){ + case AlexaAuth.AuthSignIn: + signinToAmazon(); + break; + case AlexaAuth.AuthRegisterDevice: + registerDevice(); + break; + case AlexaAuth.AuthError: + error = AlexaAuth.AutomaticAuthFailed; + break; + } + } + + Component.onCompleted: { + authPage.profile.httpAcceptLanguage = "en-US,en;q=0.9"; + httpUserAgent = authPage.profile.httpUserAgent; + } + onLoadingChanged: { + if (loadRequest.status === WebEngineLoadRequest.LoadSucceededStatus) { + callAuthAction() + } + if (loadRequest.status === WebEngineLoadRequest.LoadFailedStatus) { + if (retryCount < 2) { + retryCount += 1 + callAuthAction() + } else { + error = AlexaAuth.AutomaticAuthFailed; + } + } + } + } + + function authorize() + { + if (AlexaAuth.parseJson()) { + isAuthorizing = true; + authPage.url = authUrl; + } else { + error = AlexaAuth.ConfigFileFailure; + } + } + + function authorizeWithCaptcha() + { + isFillingCaptcha = false + inputEmail() + inputPassword() + inputCaptcha() + } + + function signinToAmazon() + { + var js = AlexaAuth.getJSString(AlexaAuth.SignIn); + authPage.runJavaScript(js, function(cb) { + switch (AlexaAuth.signinToAmazonResult(cb)) { + case AlexaAuth.SignInInputEmail: + inputEmail(); + break + case AlexaAuth.SignInCaptcha: + error = AlexaAuth.ImageRecognizionRequired; + showCaptcha() + break + case AlexaAuth.SignInError: + error = AlexaAuth.ImageRecognizionRequired; + showCaptcha(); + break + } + } + ) + } + + function showCaptcha() + { + var js = AlexaAuth.getJSString(AlexaAuth.CaptchaSrc); + authPage.runJavaScript(js, function(cb) { + if (cb === null) { + console.warn("Unable to get captcha") + } else { + root.isFillingCaptcha = true; + root.captchaUrl = cb + } + } ); + } + + function inputCaptcha() + { + var js = AlexaAuth.getJSString(AlexaAuth.GetCaptchaInput); + authPage.runJavaScript(js, function (cb) { + if (cb === null) { + console.warn("Captcha field doesn't exist on the page.") + error = AlexaAuth.HtmlItemNotFound; + } else { + var jsSet = AlexaAuth.getJSString(AlexaAuth.SetCaptcha, captcha); + authPage.runJavaScript(jsSet); + clickSignIn(); + } + } + ); + } + + function inputEmail() + { + var js = AlexaAuth.getJSString(AlexaAuth.GetEmailInput); + authPage.runJavaScript(js, function (cb) { + if (cb === null) { + console.warn("Email field doesn't exist on the page.") + error = AlexaAuth.HtmlItemNotFound; + } else { + var jsSet = AlexaAuth.getJSString(AlexaAuth.SetEmail, email); + authPage.runJavaScript(jsSet); + inputPassword(); + } + } + ); + } + + function inputPassword() + { + var js = AlexaAuth.getJSString(AlexaAuth.GetPasswordInput); + authPage.runJavaScript(js, function (cb) { + if (cb === null) { + console.warn("Password field doesn't exist on the page") + error = AlexaAuth.HtmlItemNotFound; + } else { + var jsSet = AlexaAuth.getJSString(AlexaAuth.SetPassword, password); + authPage.runJavaScript(jsSet); + clickSignIn(); + } + } + ); + } + + function clickSignIn() + { + var js = AlexaAuth.getJSString(AlexaAuth.GetClickSignIn); + authPage.runJavaScript(js, function (cb) { + if (cb === null) { + console.warn("Sign in button doesn't exist on the page") + error = AlexaAuth.HtmlItemNotFound; + } else { + var jsSet = AlexaAuth.getJSString(AlexaAuth.ClickElement, js); + authPage.runJavaScript(jsSet); + } + } + ); + } + + function registerDevice() + { + var js = AlexaAuth.getJSString(AlexaAuth.RegisterDeviceTitle); + authPage.runJavaScript(js, function(cb) { + switch (AlexaAuth.registerDeviceResult(cb)) { + case AlexaAuth.RegisterDevice: + inputCode(); + break + case AlexaAuth.RegisterDeviceSuccess: + isAuthorizing = false; + authorizationSucceed = true; + break + case AlexaAuth.RegisterDeviceError: + error = AlexaAuth.AutomaticAuthFailed; + break + } + } + ) + } + + function inputCode() + { + var js = AlexaAuth.getJSString(AlexaAuth.GetInputCode); + authPage.runJavaScript(js, function (cb) { + if (cb === null) { + console.warn("No field for authorization code!") + error = AlexaAuth.HtmlItemNotFound; + } else if (authCode.length > 0){ + var jsSet = AlexaAuth.getJSString(AlexaAuth.SetInputCode, authCode); + authPage.runJavaScript(jsSet); + clickContinue(); + } else { + error = AlexaAuth.AutomaticAuthFailed; + } + } + ); + } + + function clickContinue() + { + var js = AlexaAuth.getJSString(AlexaAuth.GetContinue); + authPage.runJavaScript(js, function (cb) { + if (cb === null) { + console.warn("Not found 'continue' button") + error = AlexaAuth.HtmlItemNotFound; + } else { + var jsSet = AlexaAuth.getJSString(AlexaAuth.ClickElement, js); + authPage.runJavaScript(jsSet); + } + } + ); + } +} diff --git a/app/Header.qml b/app/Header.qml index 18c91cb..19b2223 100644 --- a/app/Header.qml +++ b/app/Header.qml @@ -45,20 +45,17 @@ Control { property int headerTextIndex: 0 } - width: parent.width - height: Sizes.dp(100) - contentItem: Item { Row { id: animatedTextRow anchors.centerIn: parent - anchors.verticalCenterOffset: Sizes.dp(200) + anchors.verticalCenterOffset: Sizes.dp(50) spacing: Sizes.dp(35) Image { id: alexaLogo anchors.verticalCenter: parent.verticalCenter fillMode: Image.PreserveAspectFit - height: root.height*0.7 + height: root.height*0.25 source: "assets/logo.png" } Label { diff --git a/app/MainView.qml b/app/MainView.qml index af823de..78e6dd7 100644 --- a/app/MainView.qml +++ b/app/MainView.qml @@ -50,20 +50,24 @@ Item { property string neptuneState: "Maximized" - AlexaAuth { + AuthWebPageInteraction { id: alexaAuth - httpUserAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36" + onErrorChanged: { + if (error === AlexaAuth.AutomaticAuthFailed){ + authView.state = "manual_auth" + } + } } Connections { target: AlexaInterface onAuthCodeChanged: { - if (authCode !== "") { - alexaAuth.authCode = authCode + if (AlexaInterface.authCode !== "") { + alexaAuth.authCode = AlexaInterface.authCode } } onAuthUrlChanged: { - alexaAuth.authUrl = authUrl + alexaAuth.authUrl = AlexaInterface.authUrl } Component.onCompleted: { AlexaInterface.logLevel = Alexa.Debug9 @@ -71,8 +75,11 @@ Item { } Header { + id: header anchors.top: parent.top anchors.topMargin: Sizes.dp(80) + width: parent.width + height: Sizes.dp(356) anchors.horizontalCenter: parent.horizontalCenter unfoldHeader: alexaView.visible || authView.visible visible: root.neptuneState === "Maximized" @@ -80,10 +87,11 @@ Item { Item { id: paneMainView - anchors.top: parent.top + anchors.top: header.bottom anchors.left: parent.left anchors.right: parent.right - height: parent.height - Sizes.dp(50) + // to not overlap content with on-top widget + height: parent.height - (header.y + header.height) AlexaView { id: alexaView diff --git a/app/app.pro b/app/app.pro index 247f50b..7926e13 100644 --- a/app/app.pro +++ b/app/app.pro @@ -12,7 +12,8 @@ FILES += info.yaml \ Footer.qml \ WeatherCard.qml \ InfoCard.qml \ - MainView.qml + MainView.qml \ + AuthWebPageInteraction.qml app.files = $$FILES app.path = /apps/com.luxoft.alexa diff --git a/plugins/alexaauth/alexaauth.cpp b/plugins/alexaauth/alexaauth.cpp index b45b9ab..b76e495 100644 --- a/plugins/alexaauth/alexaauth.cpp +++ b/plugins/alexaauth/alexaauth.cpp @@ -31,129 +31,71 @@ #include "alexaauth.h" -#ifdef ALEXA_QT_WEBENGINE -#include -#include -#include -#include #include -#endif #include #include +#include +#include AlexaAuth::AlexaAuth(QObject *parent) : QObject(parent) { -#ifdef ALEXA_QT_WEBENGINE - m_authPage.profile()->clearHttpCache(); - m_authPage.profile()->cookieStore()->deleteAllCookies(); - m_authPage.profile()->setHttpAcceptLanguage("en-US,en;q=0.9"); - m_httpUserAgent = m_authPage.profile()->httpUserAgent(); - m_error = ErrorState::None; -#else - qDebug() << "QWebEngine not available, cannot authorize automatically."; - m_error = AlexaAuth::WebEngineNotAvailable; -#endif -} - -void AlexaAuth::setIsAuthorizing(bool isAuthorizing) -{ - if (m_isAuthorizing == isAuthorizing) - return; - - m_isAuthorizing = isAuthorizing; - emit isAuthorizingChanged(m_isAuthorizing); -} - -void AlexaAuth::setAuthCode(QString authCode) -{ - qDebug() << Q_FUNC_INFO << " " << authCode; - if (m_authCode == authCode) - return; - - m_authCode = authCode; - emit authCodeChanged(m_authCode); -} - -void AlexaAuth::setAuthUrl(QUrl authUrl) -{ - qDebug() << Q_FUNC_INFO << " " << authUrl; - if (m_authUrl == authUrl) - return; - - m_authUrl = authUrl; - emit authUrlChanged(m_authUrl); -} - -void AlexaAuth::setError(AlexaAuth::ErrorState error) -{ - if (m_error == error) - return; - - if (error != AlexaAuth::None) { - setIsAuthorizing(false); + QtWebEngine::initialize(); + QQuickWebEngineProfile::defaultProfile()->cookieStore()->deleteAllCookies(); +} + +QString AlexaAuth::getJSString(AlexaAuth::JSAuthString id, const QString &value) const +{ + QString result; + switch (id) { + case SignIn: + result = QString("document.getElementsByClassName('") + TAG_ALERT_HEADING_ID + "')[0].textContent"; + break; + case CaptchaSrc: + result = QString("document.getElementById('") + TAG_CAPTCHA_IMAGE_ID +"').src"; + break; + case GetCaptchaInput: + result = QString("document.getElementById('") + TAG_CAPTCHA_GUESS_ID + "')"; + break; + case SetCaptcha: + result = QString("document.getElementById('") + TAG_CAPTCHA_GUESS_ID + "').value='" + value + "'"; + break; + case GetEmailInput: + result = QString("document.getElementById('") + TAG_EMAIL_ID +"')"; + break; + case SetEmail: + result = QString("document.getElementById('") + TAG_EMAIL_ID + "').value='" + value+ "'"; + break; + case GetPasswordInput: + result = QString("document.getElementById('") + TAG_PASSWORD_ID +"')"; + break; + case SetPassword: + result = QString("document.getElementById('") + TAG_PASSWORD_ID + "').value='" + value + "'"; + break; + case GetClickSignIn: + result = QString("document.getElementById('") + TAG_SIGN_IN_SUBMIT_ID + "')"; + break; + case RegisterDeviceTitle: + result = QString("document.getElementById('") + TAG_SUCCESS_TITLE_ID + "').textContent"; + break; + case GetInputCode: + result = QString("document.getElementById('") + TAG_REGISTRATION_FIELD_ID + "')"; + break; + case SetInputCode: + result = QString("document.getElementById('") + TAG_REGISTRATION_FIELD_ID + "').value='" + value + "'"; + break; + case GetContinue: + result = QString("document.getElementById('") + TAG_CONTINUE_BUTTON_ID + "')"; + break; + case ClickElement: + result = value + ".click()"; + break; } - m_error = error; -#ifdef ALEXA_QT_WEBENGINE - QObject::disconnect( &m_authPage, &QWebEnginePage::loadFinished, this, &AlexaAuth::authPageLoaded); -#endif - emit errorChanged(m_error); + return result; } -void AlexaAuth::setHttpUserAgent(QString httpUserAgent) +bool AlexaAuth::parseJson() const { - qDebug() << Q_FUNC_INFO << httpUserAgent; - if (m_httpUserAgent == httpUserAgent) - return; - - m_httpUserAgent = httpUserAgent; -#ifdef ALEXA_QT_WEBENGINE - m_authPage.profile()->setHttpUserAgent(m_httpUserAgent); -#endif - emit httpUserAgentChanged(m_httpUserAgent); -} - -void AlexaAuth::setAuthorizationSucceed(bool authorizationSucceed) -{ - if (m_authorizationSucceed == authorizationSucceed) - return; - - m_authorizationSucceed = authorizationSucceed; - emit authorizationSucceedChanged(m_authorizationSucceed); -} - -void AlexaAuth::setEmail(QString email) -{ - if (m_email == email) - return; - m_email = email; - emit emailChanged(m_email); -} - -void AlexaAuth::setPassword(QString password) -{ - if (m_password == password) - return; - m_password = password; - emit passwordChanged(m_password); -} - -void AlexaAuth::authorize() -{ - qDebug() << Q_FUNC_INFO << " " << m_authUrl; -#ifdef ALEXA_QT_WEBENGINE - if (parseJson()) { - QObject::connect( &m_authPage, &QWebEnginePage::loadFinished, this, &AlexaAuth::authPageLoaded); - setIsAuthorizing(true); - m_authPage.load(m_authUrl); - } -#endif -} - -#ifdef ALEXA_QT_WEBENGINE -bool AlexaAuth::parseJson() -{ - qDebug() << Q_FUNC_INFO; if (qEnvironmentVariableIsSet("ALEXA_SDK_CONFIG_FILE")) { QFile file(qEnvironmentVariable("ALEXA_SDK_CONFIG_FILE")); if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { @@ -167,9 +109,9 @@ bool AlexaAuth::parseJson() line = in.readLine(); indx = line.indexOf("//"); if ( indx >= 0 ) { - lines += line.mid(0, indx); + lines += line.mid(0, indx); } else { - lines += line + "\n"; + lines += line + "\n"; } } file.close(); @@ -178,144 +120,55 @@ bool AlexaAuth::parseJson() if (jsonError.error != QJsonParseError::NoError){ qDebug() << "Cannot parse AlexaClientSDKConfig.json: " << jsonError.errorString(); - setError(AlexaAuth::ConfigFileFailure); return false; } return true; } else { qWarning() << "Couldn't open the config file AlexaClientSDKConfig.json"; - setError(AlexaAuth::ConfigFileFailure); return false; } } else { qWarning() << "Couldn't read the environment variable ALEXA_SDK_CONFIG_FILE"; - setError(AlexaAuth::ConfigFileFailure); return false; } } -void AlexaAuth::authPageLoaded(bool ok) +AlexaAuth::AuthStage AlexaAuth::getAuthStage(const QString &title) { - qDebug() << Q_FUNC_INFO << " " << ok << " " << m_authPage.title(); - if (ok) { - if (m_authPage.title() == HTML_TITLE_FIRST) { - QTimer::singleShot(2000, this, &AlexaAuth::signinToAmazon); - - } else if (m_authPage.title() == HTML_TITLE_SECOND) { - QTimer::singleShot(1000, this, &AlexaAuth::registerDevice); - - } else { - qWarning() << "Unknown HTML title " << m_authPage.title(); - setError(AlexaAuth::AutomaticAuthFailed); - } - } else { - qWarning() << "Something went wrong to load the auth page"; - setError(AlexaAuth::AutomaticAuthFailed); + if (title == HTML_TITLE_FIRST) { + return AuthSignIn; + } else if (title == HTML_TITLE_SECOND) { + return AuthRegisterDevice; } + qWarning() << "Unknown HTML title " << title; + return AuthError; } -void AlexaAuth::signinToAmazon() -{ - qDebug() << Q_FUNC_INFO; - m_authPage.runJavaScript(QString("document.getElementsByClassName('") + TAG_ALERT_HEADING_ID + "')[0].textContent", [this](const QVariant &cb) { - if (cb.isNull() || cb.toString() == "" || cb.toString() == HTML_ENABLE_COOKIES) { - inputEMail(); - } else if (cb.toString() == HTML_IMPORTANT_MESSAGE) { - qWarning() << "Image capture detected! Cannot proceed automatically. Please, authorize manually on " << m_authUrl; - setError(AlexaAuth::ImageRecognizionRequired); - } else { - qDebug() << "Something went wrong in " << Q_FUNC_INFO << " " << cb.toString(); - setError(AlexaAuth::AutomaticAuthFailed); - } - }); -} - -void AlexaAuth::inputEMail() +AlexaAuth::SignInResult AlexaAuth::signinToAmazonResult(const QVariant &cb) { - qDebug() << Q_FUNC_INFO; - m_authPage.runJavaScript(QString("document.getElementById('") + TAG_EMAIL_ID +"')", [this](const QVariant &cb) { - if (cb.isNull()) { - qWarning() << "Email field doesn't exist on the page."; - setError(AlexaAuth::HtmlItemNotFound); - } else { - m_authPage.runJavaScript(QString("document.getElementById('") + TAG_EMAIL_ID + "').value='" + m_email + "'"); - QTimer::singleShot(2000, this, &AlexaAuth::inputPassword); - } - }); -} - -void AlexaAuth::inputPassword() -{ - qDebug() << Q_FUNC_INFO; - m_authPage.runJavaScript(QString("document.getElementById('") + TAG_PASSWORD_ID + "')", [this](const QVariant &cb){ - if (cb.isNull()) { - qWarning() << "Password field doesn't exist on the page"; - setError(AlexaAuth::HtmlItemNotFound); - } else { - m_authPage.runJavaScript(QString("document.getElementById('") + TAG_PASSWORD_ID + "').value='" + m_password + "'"); - QTimer::singleShot(2000, this, &AlexaAuth::clickSignIn); - } - }); -} - -void AlexaAuth::clickSignIn() -{ - qDebug() << Q_FUNC_INFO; - m_authPage.runJavaScript(QString("document.getElementById('") + TAG_SIGN_IN_SUBMIT_ID + "')", [this](const QVariant &cb){ - if (cb.isNull()) { - qWarning() << "Sign in button doesn't exist on the page"; - setError(AlexaAuth::HtmlItemNotFound); - } else { - m_authPage.runJavaScript(QString("document.getElementById('") + TAG_SIGN_IN_SUBMIT_ID + "').click()"); - } - }); -} - -void AlexaAuth::registerDevice() -{ - qDebug() << Q_FUNC_INFO; - m_authPage.runJavaScript(QString("document.getElementById('") + TAG_SUCCESS_TITLE_ID + "').textContent", [this](const QVariant &cb) { - if (cb.toString() == HTML_REGISTER_DEVICE) { - inputCode(); - } else if (cb.toString() == HTML_SUCCESS) { - qDebug() << "Automatic authorization completed successfully."; - setIsAuthorizing(false); - setAuthorizationSucceed(true); - } else { - qDebug() << "Something went wrong in " << Q_FUNC_INFO << " " << cb.toString(); - setError(AlexaAuth::AutomaticAuthFailed); - } - }); -} + if (cb.isNull() || cb.toString() == "" || cb.toString() == HTML_ENABLE_COOKIES) { + return SignInInputEmail; + } else if (cb.toString() == HTML_IMPORTANT_MESSAGE) { + qWarning() << "Image capture detected!"; + return SignInCaptcha; + } else if (cb.toString() == HTML_TITLE_ERROR_CAPTCHA) { + qWarning() << "Image capture detected!, wrong captcha"; + return SignInCaptcha; + } -void AlexaAuth::inputCode() -{ - qDebug() << Q_FUNC_INFO; - m_authPage.runJavaScript(QString("document.getElementById('") + TAG_REGISTRATION_FIELD_ID + "')" , [this] (const QVariant &cb) { - if (cb.isNull()) { - qWarning() << "No field for authorization code!"; - setError(AlexaAuth::HtmlItemNotFound); - } else if (m_authCode.length() > 0) { - m_authPage.runJavaScript(QString("document.getElementById('") + TAG_REGISTRATION_FIELD_ID + "').value='" + m_authCode + "'"); - QTimer::singleShot(2000, this, &AlexaAuth::clickContinue); - } else { - qDebug() << "Authorization code was empty"; - setError(AlexaAuth::AutomaticAuthFailed); - } - }); + qDebug() << "Something went wrong in " << Q_FUNC_INFO << " " << cb.toString(); + return SignInError; } -void AlexaAuth::clickContinue() +AlexaAuth::RegisterDeviceResult AlexaAuth::registerDeviceResult(const QVariant &cb) { - qDebug() << Q_FUNC_INFO; - m_authPage.runJavaScript(QString("document.getElementById('") + TAG_CONTINUE_BUTTON_ID + "')", [this] (const QVariant &cb) { - if (cb.isNull()) { - qWarning() << "Not found 'continue' button"; - setError(AlexaAuth::HtmlItemNotFound); - } else { - m_authPage.runJavaScript(QString("document.getElementById('") + TAG_CONTINUE_BUTTON_ID + "').click()"); - } - }); - // todo: check what happens if the code was wrong + if (cb.toString() == HTML_REGISTER_DEVICE) { + return AlexaAuth::RegisterDevice; + } else if (cb.toString() == HTML_SUCCESS) { + qDebug() << "Automatic authorization completed successfully."; + return AlexaAuth::RegisterDeviceSuccess; + } else { + qDebug() << "Something went wrong in " << Q_FUNC_INFO << " " << cb.toString(); + return AlexaAuth::RegisterDeviceError; + } } -#endif diff --git a/plugins/alexaauth/alexaauth.h b/plugins/alexaauth/alexaauth.h index be81e79..b61efac 100644 --- a/plugins/alexaauth/alexaauth.h +++ b/plugins/alexaauth/alexaauth.h @@ -35,9 +35,8 @@ #include #include #include -#ifdef ALEXA_QT_WEBENGINE -#include -#include +#include + // ## Step 1, login to amazon.developer.com #define HTML_TITLE_FIRST "Amazon Sign-In" @@ -47,9 +46,12 @@ #define TAG_EMAIL_ID "ap_email" #define TAG_PASSWORD_ID "ap_password" #define TAG_SIGN_IN_SUBMIT_ID "signInSubmit" +#define TAG_CAPTCHA_IMAGE_ID "auth-captcha-image" +#define TAG_CAPTCHA_GUESS_ID "auth-captcha-guess" // ## Step 2, give authorization code #define HTML_TITLE_SECOND "Amazon Two-Step Verification" +#define HTML_TITLE_ERROR_CAPTCHA "There was a problem" #define TAG_REGISTRATION_FIELD_ID "cbl-registration-field" #define TAG_CONTINUE_BUTTON_ID "cbl-continue-button" @@ -57,25 +59,14 @@ #define HTML_REGISTER_DEVICE "Register Your Device" #define HTML_SUCCESS "Success!" #define TAG_SUCCESS_TITLE_ID "cbl-page-title" -#endif class AlexaAuth : public QObject { Q_OBJECT - Q_PROPERTY(bool isAuthorizing READ isAuthorizing NOTIFY isAuthorizingChanged) - Q_PROPERTY(QString authCode READ authCode WRITE setAuthCode NOTIFY authCodeChanged) - Q_PROPERTY(QUrl authUrl READ authUrl WRITE setAuthUrl NOTIFY authUrlChanged) - Q_PROPERTY(ErrorState error READ error NOTIFY errorChanged) - Q_PROPERTY(QString httpUserAgent READ httpUserAgent WRITE setHttpUserAgent NOTIFY httpUserAgentChanged) - Q_PROPERTY(bool authorizationSucceed READ authorizationSucceed NOTIFY authorizationSucceedChanged) - Q_PROPERTY(QString email READ email WRITE setEmail NOTIFY emailChanged) - Q_PROPERTY(QString password READ password WRITE setPassword NOTIFY passwordChanged) - public: - enum ErrorState { - None, + NoError, WebEngineNotAvailable, ConfigFileFailure, HtmlItemNotFound, @@ -84,64 +75,61 @@ public: }; Q_ENUM(ErrorState) + enum AuthStage { + AuthSignIn, + AuthRegisterDevice, + AuthError + }; + Q_ENUM(AuthStage) + + enum SignInResult { + SignInInputEmail, + SignInCaptcha, + SignInError + }; + Q_ENUM(SignInResult) + + enum RegisterDeviceResult { + RegisterDevice, + RegisterDeviceSuccess, + RegisterDeviceError + }; + Q_ENUM(RegisterDeviceResult) + + enum JSAuthString { + SignIn, + CaptchaSrc, + GetCaptchaInput, + SetCaptcha, + GetEmailInput, + SetEmail, + GetPasswordInput, + SetPassword, + GetClickSignIn, + RegisterDeviceTitle, + GetInputCode, + SetInputCode, + GetContinue, + ClickElement + }; + Q_ENUM(JSAuthString) + explicit AlexaAuth(QObject *parent = nullptr); - Q_INVOKABLE void authorize(); - - bool isAuthorizing() const { return m_isAuthorizing; } - QString authCode() const { return m_authCode; } - QUrl authUrl() const { return m_authUrl; } - ErrorState error() const { return m_error; } - QString httpUserAgent() const { return m_httpUserAgent; } - bool authorizationSucceed() const { return m_authorizationSucceed; } - QString email() const { return m_email; } - QString password() const { return m_password; } - - void setIsAuthorizing(bool isAuthorizing); - void setAuthCode(QString authCode); - void setAuthUrl(QUrl authUrl); - void setError(AlexaAuth::ErrorState error); - void setHttpUserAgent(QString httpUserAgent); - void setAuthorizationSucceed(bool authorizationSucceed); - void setEmail(QString email); - void setPassword(QString password); - - -signals: - void isAuthorizingChanged(bool isAuthorizing); - void authCodeChanged(QString authCode); - void authUrlChanged(QUrl authUrl); - void errorChanged(AlexaAuth::ErrorState error); - void httpUserAgentChanged(QString httpUserAgent); - void authorizationSucceedChanged(bool authorizationSucceed); - void emailChanged(QString email); - void passwordChanged(QString password); - -public slots: - - -private: -#ifdef ALEXA_QT_WEBENGINE - bool parseJson(); - void authPageLoaded(bool ok); - void signinToAmazon(); - void inputEMail(); - void inputPassword(); - void clickSignIn(); - void registerDevice(); - void inputCode(); - void clickContinue(); - - QWebEnginePage m_authPage; -#endif - QString m_authCode; - bool m_isAuthorizing = false; - QUrl m_authUrl; - ErrorState m_error = ErrorState::None; - QString m_httpUserAgent; - bool m_authorizationSucceed = false; - QString m_email; - QString m_password; + Q_INVOKABLE bool parseJson() const; + Q_INVOKABLE AlexaAuth::AuthStage getAuthStage(const QString &title); + Q_INVOKABLE QString getJSString(AlexaAuth::JSAuthString id, const QString &value = "") const; + Q_INVOKABLE AlexaAuth::SignInResult signinToAmazonResult(const QVariant &cb); + Q_INVOKABLE AlexaAuth::RegisterDeviceResult registerDeviceResult(const QVariant &cb); }; +static QObject *alexaAuthSingletonProvider(QQmlEngine *engine, QJSEngine *scriptEngine) +{ + Q_UNUSED(engine) + Q_UNUSED(scriptEngine) + + AlexaAuth *singletonObject = new AlexaAuth(); + return singletonObject; +} + #endif // ALEXAAUTH_H diff --git a/plugins/alexaauth/alexaauth.pro b/plugins/alexaauth/alexaauth.pro index fdd2c14..f075d0f 100644 --- a/plugins/alexaauth/alexaauth.pro +++ b/plugins/alexaauth/alexaauth.pro @@ -1,20 +1,6 @@ TEMPLATE = lib TARGET = alexaauth -QT += qml quick - -# Is Qt Application manager compiled with 'enable-widgets' configuration. -# Do not change without recompiling the Qt Application Manger. -QAPPMAN_ENABLES_WIDGETS = 0 - -equals(QAPPMAN_ENABLES_WIDGETS, 1) { - qtHaveModule(webenginewidgets) { - QT += webenginewidgets - DEFINES += ALEXA_QT_WEBENGINE - } - else { - message("Qt module webenginewidgets is not available.") - } -} +QT += qml quick webengine CONFIG += plugin c++14 diff --git a/plugins/alexaauth/alexaauth_plugin.cpp b/plugins/alexaauth/alexaauth_plugin.cpp index 03f31a8..94e8728 100644 --- a/plugins/alexaauth/alexaauth_plugin.cpp +++ b/plugins/alexaauth/alexaauth_plugin.cpp @@ -36,6 +36,6 @@ void AlexaAuthPlugin::registerTypes(const char *uri) { - qmlRegisterType(uri, 1, 0, "AlexaAuth"); + qmlRegisterSingletonType(uri, 1, 0, "AlexaAuth", alexaAuthSingletonProvider); } -- cgit v1.2.3