Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 20 additions & 18 deletions www/addons/mod/feedback/services/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
13 changes: 4 additions & 9 deletions www/addons/mod/feedback/templates/form.html
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,10 @@
</script>

<script type="text/ng-template" id="captcha">
<div ng-if="!preview && item.captcha && item.captcha.challengehash && item.captcha.imageurl && !offline">
<img ng-src="{{item.captcha.imageurl}}" alt="{{ 'mm.login.recaptchachallengeimage' | translate }}">
<ion-input class="item-input">
<input type="text" name="recaptcharesponse" placeholder="{{ 'mm.login.enterthewordsabove' | translate }}" ng-model="item.value" autocapitalize="none" autocorrect="off" required>
</ion-input>
<!-- Use anchor instead of button to prevent marking form as submitted. -->
<a class="button button-block" ng-click="requestCaptcha(item)">{{ 'mm.login.getanothercaptcha' | translate }}</a>
</div>
<div ng-if="!preview && (!item.captcha || !item.captcha.challengehash || !item.captcha.imageurl || offline)" class="mm-warning-card-icon">
<!-- ReCaptcha element. -->
<mm-recaptcha ng-if="!preview && !offline" publickey="{{item.captcha.recaptchapublickey}}" challengehash="{{item.captcha.challengehash}}" challengeimage="{{item.captcha.imageurl}}" model="item" model-value-name="value" request-captcha="requestCaptcha(item)"></mm-recaptcha>
<!-- ReCaptcha not supported warning. -->
<div ng-if="!preview && (!item.captcha || offline)" class="mm-warning-card-icon">
<i class="icon ion-alert-circled padding"></i> {{ 'mma.mod_feedback.captchaofflinewarning' | translate }}
</div>
</script>
Expand Down
39 changes: 28 additions & 11 deletions www/core/components/login/controllers/emailsignup.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {};
Expand Down Expand Up @@ -74,14 +75,18 @@ 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;
}
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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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();
});
Expand Down
2 changes: 2 additions & 0 deletions www/core/components/login/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
14 changes: 3 additions & 11 deletions www/core/components/login/templates/emailsignup.html
Original file line number Diff line number Diff line change
Expand Up @@ -59,18 +59,10 @@
<mm-user-profile-field ng-repeat="field in category.fields" field="field" edit="true" signup="true" register-auth="email" model="data" scroll-handle="mmLoginEmailSignupScroll"></mm-user-profile-field>
</div>
<!-- ReCAPTCHA -->
<div ng-if="settings.recaptchachallengehash && settings.recaptchachallengeimage">
<div ng-if="settings.recaptchapublickey">
<div class="item item-divider item-text-wrap">{{ 'mm.login.security_question' | translate }}</div>
<div class="item">
<img ng-src="{{settings.recaptchachallengeimage}}" alt="{{ 'mm.login.recaptchachallengeimage' | translate }}">
</div>
<ion-input class="item item-input item-stacked-label item-text-wrap" mm-input-errors>
<ion-label mm-mark-required="true">{{ 'mm.login.enterthewordsabove' | translate }}</ion-label>
<input type="text" name="recaptcharesponse" placeholder="{{ 'mm.login.enterthewordsabove' | translate }}" ng-model="data.recaptcharesponse" autocapitalize="none" autocorrect="off" required>
</ion-input>
<div class="item padding">
<!-- Use anchor instead of button to prevent marking form as submitted. -->
<a class="button button-block" ng-click="requestCaptcha()">{{ 'mm.login.getanothercaptcha' | translate }}</a>
<div class="item item-text-wrap">
<mm-recaptcha publickey="{{settings.recaptchapublickey}}" challengehash="{{settings.recaptchachallengehash}}" challengeimage="{{settings.recaptchachallengeimage}}" model="data" siteurl="{{siteurl}}" request-captcha="requestCaptchaV1()"></mm-recaptcha>
</div>
</div>
<!-- Site policy (if any). -->
Expand Down
5 changes: 4 additions & 1 deletion www/core/directives/iframe.js
Original file line number Diff line number Diff line change
Expand Up @@ -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%.
*/
Expand Down Expand Up @@ -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.
Expand All @@ -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.
});

Expand Down
137 changes: 137 additions & 0 deletions www/core/directives/recaptcha.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// (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.
* @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.
* @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');

/**
* 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 && !scope.initializedV2) {
scope.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[scope.modelValueName] = value;
scope.closeModal();
};
contentWindow.recaptchaexpiredcallback = function() {
scope.expired = true;
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 && !scope.initializedV1) {
scope.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: '@',
modelValueName: '@?',
siteurl: '@?',
challengehash: '@?',
challengeimage: '@?',
requestCaptcha: '&?'
},
link: function(scope) {
scope.siteurl = scope.siteurl || $mmSite.getURL();
scope.modelValueName = scope.modelValueName || 'recaptcharesponse';
scope.initializedV2 = false;
scope.initializedV1 = false;

setupCaptcha(scope);

// If any of the values change, setup the captcha.
scope.$watchGroup(['publickey', 'challengehash', 'challengeimage'], function() {
setupCaptcha(scope);
});
}
};
});
2 changes: 2 additions & 0 deletions www/core/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
"accounts": "Accounts",
"allparticipants": "All participants",
"android": "Android",
"answer": "Answer",
"answered": "Answered",
"areyousure": "Are you sure?",
"back": "Back",
"cancel": "Cancel",
Expand Down
18 changes: 16 additions & 2 deletions www/core/scss/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
Loading