From 8eb1c4b4852e9f87f768d249630bd7a50ab0b6b0 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 6 Mar 2018 13:05:56 +0100 Subject: [PATCH 1/2] MOBILE-1900 login: Support V2 recaptcha in signup --- .../login/controllers/emailsignup.js | 39 +++-- www/core/components/login/lang/en.json | 2 + .../login/templates/emailsignup.html | 15 +- www/core/directives/iframe.js | 5 +- www/core/directives/recaptcha.js | 135 ++++++++++++++++++ www/core/lang/en.json | 2 + www/core/scss/styles.scss | 18 ++- www/core/templates/recaptcha.html | 27 ++++ www/core/templates/recaptchamodal.html | 9 ++ 9 files changed, 224 insertions(+), 28 deletions(-) create mode 100644 www/core/directives/recaptcha.js create mode 100644 www/core/templates/recaptcha.html create mode 100644 www/core/templates/recaptchamodal.html diff --git a/www/core/components/login/controllers/emailsignup.js b/www/core/components/login/controllers/emailsignup.js index 241a0b9c3f9..d87eba473fc 100644 --- a/www/core/components/login/controllers/emailsignup.js +++ b/www/core/components/login/controllers/emailsignup.js @@ -26,7 +26,8 @@ angular.module('mm.core.login') var siteConfig, modalInitialized = false, - scrollView = $ionicScrollDelegate.$getByHandle('mmLoginEmailSignupScroll'); + scrollView = $ionicScrollDelegate.$getByHandle('mmLoginEmailSignupScroll'), + recaptchaV1Enabled = false; $scope.siteurl = $stateParams.siteurl; $scope.data = {}; @@ -74,6 +75,8 @@ angular.module('mm.core.login') $scope.settings = settings; $scope.countries = $mmUtil.getCountryList(); $scope.categories = $mmLoginHelper.formatProfileFieldsForSignup(settings.profilefields); + recaptchaV1Enabled = !!(settings.recaptchapublickey && settings.recaptchachallengehash && + settings.recaptchachallengeimage); if (settings.defaultcity && !$scope.data.city) { $scope.data.city = settings.defaultcity; @@ -81,7 +84,9 @@ angular.module('mm.core.login') if (settings.country && !$scope.data.country) { $scope.data.country = settings.country; } - $scope.data.recaptcharesponse = ''; // Reset captcha. + if (recaptchaV1Enabled) { + $scope.data.recaptcharesponse = ''; // Reset captcha. + } $scope.namefieldsErrors = {}; angular.forEach(settings.namefields, function(field) { @@ -123,8 +128,8 @@ angular.module('mm.core.login') }); }; - // Request another captcha. - $scope.requestCaptcha = function(ignoreError) { + // Request another captcha (V1). + $scope.requestCaptchaV1 = function(ignoreError) { var modal = $mmUtil.showModalLoading(); getSignupSettings().catch(function(err) { if (!ignoreError && err) { @@ -164,10 +169,13 @@ angular.module('mm.core.login') params.redirect = $mmLoginHelper.prepareForSSOLogin($scope.siteurl, service, siteConfig.launchurl); } - if ($scope.settings.recaptchachallengehash && $scope.settings.recaptchachallengeimage) { - params.recaptchachallengehash = $scope.settings.recaptchachallengehash; + // Get the recaptcha response (if needed). + if ($scope.data.recaptcharesponse) { params.recaptcharesponse = $scope.data.recaptcharesponse; } + if ($scope.settings.recaptchachallengehash) { + params.recaptchachallengehash = $scope.settings.recaptchachallengehash; + } // Get the data for the custom profile fields. $mmUserProfileFieldsDelegate.getDataForFields(fields, true, 'email', $scope.data).then(function(fieldsData) { @@ -180,20 +188,29 @@ angular.module('mm.core.login') $ionicHistory.goBack(); } else { if (result.warnings && result.warnings.length) { - $mmUtil.showErrorModal(result.warnings[0].message); + var error = result.warnings[0].message; + if (error == 'incorrect-captcha-sol') { + error = $translate.instant('mm.login.recaptchaincorrect'); + } + + $mmUtil.showErrorModal(error); } else { $mmUtil.showErrorModal('mm.login.usernotaddederror', true); } - // Error sending, request another capctha since the current one is probably invalid now. - $scope.requestCaptcha(true); + if (recaptchaV1Enabled) { + // Error sending, request another capctha since the current one is probably invalid now. + $scope.requestCaptchaV1(true); + } } }); }).catch(function(error) { $mmUtil.showErrorModalDefault(error && error.error, 'mm.login.usernotaddederror', true); - // Error sending, request another capctha since the current one is probably invalid now. - $scope.requestCaptcha(true); + if (recaptchaV1Enabled) { + // Error sending, request another capctha since the current one is probably invalid now. + $scope.requestCaptchaV1(true); + } }).finally(function() { modal.dismiss(); }); diff --git a/www/core/components/login/lang/en.json b/www/core/components/login/lang/en.json index 0d40aa9fc49..94bfcc156ae 100644 --- a/www/core/components/login/lang/en.json +++ b/www/core/components/login/lang/en.json @@ -58,6 +58,8 @@ "problemconnectingerrorcontinue": "Double check you've entered the address correctly and try again.", "profileinvaliddata": "Invalid value", "recaptchachallengeimage": "reCAPTCHA challenge image", + "recaptchaexpired": "Verification expired. Answer the security question again.", + "recaptchaincorrect": "The security question answer is incorrect.", "reconnect": "Reconnect", "reconnectdescription": "Your authentication token is invalid or has expired. You have to reconnect to the site.", "reconnectssodescription": "Your authentication token is invalid or has expired. You have to reconnect to the site. You need to log in to the site in a browser window.", diff --git a/www/core/components/login/templates/emailsignup.html b/www/core/components/login/templates/emailsignup.html index 0c6c8cf2dcd..5b7d5d2cf24 100644 --- a/www/core/components/login/templates/emailsignup.html +++ b/www/core/components/login/templates/emailsignup.html @@ -59,20 +59,7 @@ -
-
{{ 'mm.login.security_question' | translate }}
-
- {{ 'mm.login.recaptchachallengeimage' | translate }} -
- - {{ 'mm.login.enterthewordsabove' | translate }} - - - -
+
{{ 'mm.login.policyagreement' | translate }}
diff --git a/www/core/directives/iframe.js b/www/core/directives/iframe.js index 860c5556934..6989dda34b8 100644 --- a/www/core/directives/iframe.js +++ b/www/core/directives/iframe.js @@ -26,6 +26,7 @@ angular.module('mm.core') * Accepts the following attributes: * * @param {String} src The source of the iframe. + * @param {Function} [loaded] Function to call when the iframe is loaded. * @param {Mixed} [width=100%] Width of the iframe. If not defined, use 100%. * @param {Mixed} [height=100%] Height of the iframe. If not defined, use 100%. */ @@ -199,7 +200,8 @@ angular.module('mm.core') restrict: 'E', templateUrl: 'core/templates/iframe.html', scope: { - src: '=' + src: '=', + loaded: '&?' }, link: function(scope, element, attrs) { var url = (scope.src && scope.src.toString()) || '', // Convert $sce URLs to string URLs. @@ -216,6 +218,7 @@ angular.module('mm.core') if (scope.loading) { iframe.on('load', function() { scope.loading = false; + scope.loaded && scope.loaded(); // Notify iframe was loaded. $timeout(); // Use $timeout to force a digest and update the view. }); diff --git a/www/core/directives/recaptcha.js b/www/core/directives/recaptcha.js new file mode 100644 index 00000000000..6b45f52b442 --- /dev/null +++ b/www/core/directives/recaptcha.js @@ -0,0 +1,135 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +angular.module('mm.core') + +/** + * Directive to display a reCaptcha. + * + * @module mm.core + * @ngdoc directive + * @name mmRecaptcha + * @description + * Accepts the following attributes: + * + * @param {Object} model The model where to store the recaptcha response. It will be stored in model.recaptcharesponse. + * @param {String} publickey The site public key. + * @param {String} [siteurl] The site URL. If not defined, current site. + * @param {String} [challengehash] The recaptcha challenge hash. Required for V1. + * @param {String} [challengeimage] The recaptcha challenge image. Required for V1. + * @param {Function} [requestCaptcha] Function to called to request another captcha. Only for V1. + */ +.directive('mmRecaptcha', function($log, $mmLang, $mmSite, $mmFS, $sce, $ionicModal, $timeout) { + $log = $log.getInstance('mmIframe'); + + var initializedV2 = false, + initializedV1 = false; + + /** + * Setup the data and functions for the captcha. + * + * @param {Object} scope Directive scope. + */ + function setupCaptcha(scope) { + // Check if recaptcha is enabled (and which version). + scope.recaptchaV1Enabled = !!(scope.publickey && scope.challengehash && scope.challengeimage); + scope.recaptchaV2Enabled = !!(scope.publickey && !scope.challengehash && !scope.challengeimage); + + if (scope.recaptchaV2Enabled && !initializedV2) { + initializedV2 = true; + + // Get the current language of the app. + $mmLang.getCurrentLanguage().then(function(lang) { + // Set the iframe src. We use an iframe because reCaptcha V2 doesn't work with file:// protocol. + var untrustedUrl = $mmFS.concatenatePaths(scope.siteurl, 'webservice/recaptcha.php?lang=' + lang); + scope.iframeSrc = $sce.trustAsResourceUrl(untrustedUrl); + }); + + // Modal to answer the recaptcha. This is because the size of the recaptcha is dynamic, so it could + // cause problems if it was displayed inline. + $ionicModal.fromTemplateUrl('core/templates/recaptchamodal.html', { + scope: scope, + animation: 'slide-in-up' + }).then(function(m) { + scope.modal = m; + }); + + // Close the recaptcha modal. + scope.closeModal = function(){ + scope.modal.hide(); + }; + + // Open the recaptcha modal. + scope.answerRecaptchaV2 = function() { + scope.modal.show(); + }; + + // The iframe with the recaptcha was loaded. + scope.iframeLoaded = function() { + // Search the iframe. + var iframe = scope.modal.modalEl.querySelector('iframe'), + contentWindow = iframe && iframe.contentWindow; + + if (contentWindow) { + // Set the callbacks we're interested in. + contentWindow.recaptchacallback = function(value) { + scope.expired = false; + scope.model.recaptcharesponse = value; + scope.closeModal(); + }; + contentWindow.recaptchaexpiredcallback = function() { + scope.expired = true; + scope.model.recaptcharesponse = ''; + $timeout(); // Use $timeout to force a digest and update the view. + }; + // Verification expired. Check the checkbox again. + } + }; + } else if (scope.recaptchaV1Enabled && !initializedV1) { + initializedV1 = true; + + // Set the function to request another captcha. + scope.requestCaptchaV1 = function() { + scope.requestCaptcha && scope.requestCaptcha(); + }; + } + + scope.$on('$destroy', function() { + scope.modal && scope.modal.remove(); + }); + } + + return { + restrict: 'E', + templateUrl: 'core/templates/recaptcha.html', + scope: { + model: '=', + publickey: '@', + siteurl: '@?', + challengehash: '@?', + challengeimage: '@?', + requestCaptcha: '&?' + }, + link: function(scope) { + scope.siteurl = scope.siteurl || $mmSite.getURL(); + + setupCaptcha(scope); + + // If any of the values change, setup the captcha. + scope.$watchGroup(['publickey', 'challengehash', 'challengeimage'], function() { + setupCaptcha(scope); + }); + } + }; +}); diff --git a/www/core/lang/en.json b/www/core/lang/en.json index a04acb91ed1..52e8c6fa4e0 100644 --- a/www/core/lang/en.json +++ b/www/core/lang/en.json @@ -2,6 +2,8 @@ "accounts": "Accounts", "allparticipants": "All participants", "android": "Android", + "answer": "Answer", + "answered": "Answered", "areyousure": "Are you sure?", "back": "Back", "cancel": "Cancel", diff --git a/www/core/scss/styles.scss b/www/core/scss/styles.scss index 6a699a39f76..5aba26f8663 100644 --- a/www/core/scss/styles.scss +++ b/www/core/scss/styles.scss @@ -1813,10 +1813,24 @@ ol.list-with-style, ul.list-with-style { padding-bottom: 0; } -.text-success { +.text-success, p.text-success, .item p.text-success { color: $mm-success-color; } -.text-danger { +.text-danger, p.text-danger, .item p.text-danger { color: $mm-error-color; } + +// ReCaptcha modal. +.mm-recaptcha-modal { + background-color: $gray-light; + + .bar-header { + @include bar-style($bar-content-bg, $bar-content-border, $bar-content-text); + + .button { + color: $bar-content-text; + font-weight: normal; + } + } +} diff --git a/www/core/templates/recaptcha.html b/www/core/templates/recaptcha.html new file mode 100644 index 00000000000..2b30bdbb7ad --- /dev/null +++ b/www/core/templates/recaptcha.html @@ -0,0 +1,27 @@ + +
+
{{ 'mm.login.security_question' | translate }}
+
+ {{ 'mm.login.recaptchachallengeimage' | translate }} +
+ + {{ 'mm.login.enterthewordsabove' | translate }} + + + +
+ + +
+
{{ 'mm.login.security_question' | translate }}
+
+ + + {{ 'mm.core.answer' | translate }} +

{{ 'mm.core.answered' | translate }}

+

{{ 'mm.login.recaptchaexpired' | translate }}

+
+
\ No newline at end of file diff --git a/www/core/templates/recaptchamodal.html b/www/core/templates/recaptchamodal.html new file mode 100644 index 00000000000..e11fc1699b0 --- /dev/null +++ b/www/core/templates/recaptchamodal.html @@ -0,0 +1,9 @@ + + +

{{ 'mm.login.security_question' | translate }}

+ +
+ + + +
\ No newline at end of file From c7c3db429be02cc0c66ff1bf40d916a6a882cee2 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 6 Mar 2018 15:55:19 +0100 Subject: [PATCH 2/2] MOBILE-1900 feedback: Support V2 recaptcha in feedback --- www/addons/mod/feedback/services/helper.js | 38 ++++++++++--------- www/addons/mod/feedback/templates/form.html | 13 ++----- .../login/templates/emailsignup.html | 7 +++- www/core/directives/recaptcha.js | 22 ++++++----- www/core/templates/recaptcha.html | 35 ++++++++--------- 5 files changed, 58 insertions(+), 57 deletions(-) diff --git a/www/addons/mod/feedback/services/helper.js b/www/addons/mod/feedback/services/helper.js index 1ab0a761512..c8f3302ac4e 100644 --- a/www/addons/mod/feedback/services/helper.js +++ b/www/addons/mod/feedback/services/helper.js @@ -269,7 +269,26 @@ angular.module('mm.addons.mod_feedback') angular.forEach(items, function(itemData) { itemData.hasError = false; - if (itemData.hasvalue) { + + if (itemData.typ == "captcha") { + var value = itemData.value || "", + name = itemData.typ + '_' + itemData.id, + answered = false; + + answered = !!value; + responses[name] = 1; + responses.recaptcha_challenge_field = itemData.captcha && itemData.captcha.challengehash; + responses.recaptcha_response_field = value; + responses['g-recaptcha-response'] = value; + responses.recaptcha_element = 'dummyvalue'; + + if (itemData.required && !answered) { + // Check if it has any value. + itemData.isEmpty = true; + } else { + itemData.isEmpty = false; + } + } else if (itemData.hasvalue) { var name, value, nameTemp = itemData.typ + '_' + itemData.id, answered = false; @@ -310,23 +329,6 @@ angular.module('mm.addons.mod_feedback') responses[name] = value; } - if (itemData.required && !answered) { - // Check if it has any value. - itemData.isEmpty = true; - } else { - itemData.isEmpty = false; - } - } else if (itemData.typ == "captcha") { - var value = itemData.value || "", - name = itemData.typ + '_' + itemData.id, - answered = false; - - answered = !!value; - responses[name] = 1; - responses.recaptcha_challenge_field = itemData.captcha && itemData.captcha.challengehash; - responses.recaptcha_response_field = value; - responses.recaptcha_element = 'dummyvalue'; - if (itemData.required && !answered) { // Check if it has any value. itemData.isEmpty = true; diff --git a/www/addons/mod/feedback/templates/form.html b/www/addons/mod/feedback/templates/form.html index 1639577783b..3bcba2c8b43 100644 --- a/www/addons/mod/feedback/templates/form.html +++ b/www/addons/mod/feedback/templates/form.html @@ -55,15 +55,10 @@ diff --git a/www/core/components/login/templates/emailsignup.html b/www/core/components/login/templates/emailsignup.html index 5b7d5d2cf24..7ec58ed5835 100644 --- a/www/core/components/login/templates/emailsignup.html +++ b/www/core/components/login/templates/emailsignup.html @@ -59,7 +59,12 @@
- +
+
{{ 'mm.login.security_question' | translate }}
+
+ +
+
{{ 'mm.login.policyagreement' | translate }}
diff --git a/www/core/directives/recaptcha.js b/www/core/directives/recaptcha.js index 6b45f52b442..c1779acbee0 100644 --- a/www/core/directives/recaptcha.js +++ b/www/core/directives/recaptcha.js @@ -23,8 +23,9 @@ angular.module('mm.core') * @description * Accepts the following attributes: * - * @param {Object} model The model where to store the recaptcha response. It will be stored in model.recaptcharesponse. + * @param {Object} model The model where to store the recaptcha response. * @param {String} publickey The site public key. + * @param {Object} [modelValueName] Name of the model property where to store the response. Defaults to 'recaptcharesponse'. * @param {String} [siteurl] The site URL. If not defined, current site. * @param {String} [challengehash] The recaptcha challenge hash. Required for V1. * @param {String} [challengeimage] The recaptcha challenge image. Required for V1. @@ -33,9 +34,6 @@ angular.module('mm.core') .directive('mmRecaptcha', function($log, $mmLang, $mmSite, $mmFS, $sce, $ionicModal, $timeout) { $log = $log.getInstance('mmIframe'); - var initializedV2 = false, - initializedV1 = false; - /** * Setup the data and functions for the captcha. * @@ -46,8 +44,8 @@ angular.module('mm.core') scope.recaptchaV1Enabled = !!(scope.publickey && scope.challengehash && scope.challengeimage); scope.recaptchaV2Enabled = !!(scope.publickey && !scope.challengehash && !scope.challengeimage); - if (scope.recaptchaV2Enabled && !initializedV2) { - initializedV2 = true; + if (scope.recaptchaV2Enabled && !scope.initializedV2) { + scope.initializedV2 = true; // Get the current language of the app. $mmLang.getCurrentLanguage().then(function(lang) { @@ -85,19 +83,19 @@ angular.module('mm.core') // Set the callbacks we're interested in. contentWindow.recaptchacallback = function(value) { scope.expired = false; - scope.model.recaptcharesponse = value; + scope.model[scope.modelValueName] = value; scope.closeModal(); }; contentWindow.recaptchaexpiredcallback = function() { scope.expired = true; - scope.model.recaptcharesponse = ''; + scope.model[scope.modelValueName] = ''; $timeout(); // Use $timeout to force a digest and update the view. }; // Verification expired. Check the checkbox again. } }; - } else if (scope.recaptchaV1Enabled && !initializedV1) { - initializedV1 = true; + } else if (scope.recaptchaV1Enabled && !scope.initializedV1) { + scope.initializedV1 = true; // Set the function to request another captcha. scope.requestCaptchaV1 = function() { @@ -116,6 +114,7 @@ angular.module('mm.core') scope: { model: '=', publickey: '@', + modelValueName: '@?', siteurl: '@?', challengehash: '@?', challengeimage: '@?', @@ -123,6 +122,9 @@ angular.module('mm.core') }, link: function(scope) { scope.siteurl = scope.siteurl || $mmSite.getURL(); + scope.modelValueName = scope.modelValueName || 'recaptcharesponse'; + scope.initializedV2 = false; + scope.initializedV1 = false; setupCaptcha(scope); diff --git a/www/core/templates/recaptcha.html b/www/core/templates/recaptcha.html index 2b30bdbb7ad..6cbec6f0143 100644 --- a/www/core/templates/recaptcha.html +++ b/www/core/templates/recaptcha.html @@ -1,27 +1,24 @@
-
{{ 'mm.login.security_question' | translate }}
-
- {{ 'mm.login.recaptchachallengeimage' | translate }} -
- + {{ 'mm.login.recaptchachallengeimage' | translate }} + {{ 'mm.login.enterthewordsabove' | translate }} - + - + + {{ 'mm.login.getanothercaptcha' | translate }}
-
{{ 'mm.login.security_question' | translate }}
-
- - - {{ 'mm.core.answer' | translate }} -

{{ 'mm.core.answered' | translate }}

-

{{ 'mm.login.recaptchaexpired' | translate }}

-
-
\ No newline at end of file + + + {{ 'mm.core.answer' | translate }} +

{{ 'mm.core.answered' | translate }}

+

{{ 'mm.login.recaptchaexpired' | translate }}

+
+ + +
+ {{ 'mm.core.errorloadingcontent' | translate }} +