diff --git a/src/app/app.scss b/src/app/app.scss index 31ac3d32f..6a58024c6 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -603,7 +603,8 @@ textarea { @extend .core-circle:before; color: $color-base; } - .text-#{$color-name} { + + .text-#{$color-name}, p.#{$color-name}, .item p.text-#{$color-name} { color: $color-base; } } @@ -626,4 +627,15 @@ textarea { [ion-fixed] { width: 100%; -} \ No newline at end of file +} + +.core-modal-fullscreen { + .modal-wrapper { + position: absolute; + top: 0 !important; + left: 0 !important; + display: block; + width: 100% !important; + height: 100% !important; + } +} diff --git a/src/components/components.module.ts b/src/components/components.module.ts index 2ede4477b..f1de97468 100644 --- a/src/components/components.module.ts +++ b/src/components/components.module.ts @@ -41,6 +41,7 @@ import { CoreNavBarButtonsComponent } from './navbar-buttons/navbar-buttons'; import { CoreDynamicComponent } from './dynamic-component/dynamic-component'; import { CoreSendMessageFormComponent } from './send-message-form/send-message-form'; import { CoreTimerComponent } from './timer/timer'; +import { CoreRecaptchaComponent, CoreRecaptchaModalComponent } from './recaptcha/recaptcha'; @NgModule({ declarations: [ @@ -67,11 +68,14 @@ import { CoreTimerComponent } from './timer/timer'; CoreNavBarButtonsComponent, CoreDynamicComponent, CoreSendMessageFormComponent, - CoreTimerComponent + CoreTimerComponent, + CoreRecaptchaComponent, + CoreRecaptchaModalComponent ], entryComponents: [ CoreContextMenuPopoverComponent, - CoreCoursePickerMenuPopoverComponent + CoreCoursePickerMenuPopoverComponent, + CoreRecaptchaModalComponent ], imports: [ IonicModule, @@ -101,7 +105,8 @@ import { CoreTimerComponent } from './timer/timer'; CoreNavBarButtonsComponent, CoreDynamicComponent, CoreSendMessageFormComponent, - CoreTimerComponent + CoreTimerComponent, + CoreRecaptchaComponent ] }) export class CoreComponentsModule {} diff --git a/src/components/iframe/iframe.html b/src/components/iframe/iframe.html index 69ace4cf8..a94b5d110 100644 --- a/src/components/iframe/iframe.html +++ b/src/components/iframe/iframe.html @@ -1,4 +1,6 @@
- + + +
\ No newline at end of file diff --git a/src/components/iframe/iframe.scss b/src/components/iframe/iframe.scss index 562b80430..8ebb42eb7 100644 --- a/src/components/iframe/iframe.scss +++ b/src/components/iframe/iframe.scss @@ -5,4 +5,25 @@ core-iframe { iframe { border: 0; } + + .core-loading-container { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + display: table; + height: 100%; + width: 100%; + z-index: 1; + margin: 0; + padding: 0; + clear: both; + + .core-loading-spinner { + display: table-cell; + text-align: center; + vertical-align: middle; + } + } } diff --git a/src/components/iframe/iframe.ts b/src/components/iframe/iframe.ts index fa61c91fe..67ed9ed94 100644 --- a/src/components/iframe/iframe.ts +++ b/src/components/iframe/iframe.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input, OnInit, ViewChild, ElementRef } from '@angular/core'; +import { Component, Input, Output, OnInit, ViewChild, ElementRef, EventEmitter } from '@angular/core'; import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; import { Platform } from 'ionic-angular'; import { CoreFileProvider } from '@providers/file'; @@ -35,6 +35,7 @@ export class CoreIframeComponent implements OnInit { @Input() src: string; @Input() iframeWidth: string; @Input() iframeHeight: string; + @Output() loaded?: EventEmitter = new EventEmitter(); loading: boolean; safeUrl: SafeResourceUrl; @@ -46,6 +47,7 @@ export class CoreIframeComponent implements OnInit { private textUtils: CoreTextUtilsProvider, private utils: CoreUtilsProvider, private domUtils: CoreDomUtilsProvider, private sitesProvider: CoreSitesProvider, private platform: Platform, private sanitizer: DomSanitizer) { this.logger = logger.getInstance('CoreIframe'); + this.loaded = new EventEmitter(); } /** @@ -66,6 +68,7 @@ export class CoreIframeComponent implements OnInit { if (this.loading) { iframe.addEventListener('load', () => { this.loading = false; + this.loaded.emit(iframe); // Notify iframe was loaded. }); iframe.addEventListener('error', () => { diff --git a/src/components/recaptcha/recaptcha.html b/src/components/recaptcha/recaptcha.html new file mode 100644 index 000000000..a0688a12b --- /dev/null +++ b/src/components/recaptcha/recaptcha.html @@ -0,0 +1,14 @@ + +
+ + + +

{{ 'core.answered' | translate }}

+

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

+
+ + +
+ + {{ 'core.errorloadingcontent' | translate }} +
\ No newline at end of file diff --git a/src/components/recaptcha/recaptcha.ts b/src/components/recaptcha/recaptcha.ts new file mode 100644 index 000000000..d8f2f76af --- /dev/null +++ b/src/components/recaptcha/recaptcha.ts @@ -0,0 +1,127 @@ +// (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. + +import { Component, Input } from '@angular/core'; +import { ModalController, ViewController, NavParams } from 'ionic-angular'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreLangProvider } from '@providers/lang'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; + +/** + * Directive to display a reCaptcha. + * + * Accepts the following attributes: + * @param {any} model The model where to store the recaptcha response. + * @param {string} publicKey The site public key. + * @param {string} [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. + */ +@Component({ + selector: 'core-recaptcha', + templateUrl: 'recaptcha.html' +}) +export class CoreRecaptchaComponent { + expired = false; + + protected lang: string; + + @Input() model: any; + @Input() publicKey: string; + @Input() modelValueName = 'recaptcharesponse'; + @Input() siteUrl?: string; + + constructor(private sitesProvider: CoreSitesProvider, langProvider: CoreLangProvider, + private textUtils: CoreTextUtilsProvider, private modalCtrl: ModalController) { + + // Get the current language of the app. + langProvider.getCurrentLanguage().then((lang) => { + this.lang = lang; + }); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.siteUrl = this.siteUrl || this.sitesProvider.getCurrentSite().getURL(); + } + + /** + * Open the recaptcha modal. + */ + answerRecaptcha(): void { + // Set the iframe src. We use an iframe because reCaptcha V2 doesn't work with file:// protocol. + const src = this.textUtils.concatenatePaths(this.siteUrl, 'webservice/recaptcha.php?lang=' + this.lang); + + // 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. + const modal = this.modalCtrl.create(CoreRecaptchaModalComponent, { src: src }, + { cssClass: 'core-modal-fullscreen'}); + modal.onDidDismiss((data) => { + this.expired = data.expired; + this.model[this.modelValueName] = data.value; + }); + modal.present(); + } +} + +@Component({ + selector: 'core-recaptcha', + templateUrl: 'recaptchamodal.html' +}) +export class CoreRecaptchaModalComponent { + + expired = false; + value = ''; + src: string; + + constructor(protected viewCtrl: ViewController, params: NavParams) { + this.src = params.get('src'); + } + + /** + * Close modal. + */ + closeModal(): void { + this.viewCtrl.dismiss({ + expired: this.expired, + value: this.value + }); + } + + /** + * The iframe with the recaptcha was loaded. + * + * @param {HTMLIFrameElement} iframe Iframe element. + */ + loaded(iframe: HTMLIFrameElement): void { + // Search the iframe content. + const contentWindow = iframe && iframe.contentWindow; + + if (contentWindow) { + // Set the callbacks we're interested in. + contentWindow['recaptchacallback'] = (value): void => { + this.expired = false; + this.value = value; + this.closeModal(); + }; + + contentWindow['recaptchaexpiredcallback'] = (): void => { + // Verification expired. Check the checkbox again. + this.expired = true; + this.value = ''; + }; + } + } +} diff --git a/src/components/recaptcha/recaptchamodal.html b/src/components/recaptcha/recaptchamodal.html new file mode 100644 index 000000000..8acfd27be --- /dev/null +++ b/src/components/recaptcha/recaptchamodal.html @@ -0,0 +1,14 @@ + + + {{ 'core.login.security_question' | translate }} + + + + + + + + + \ No newline at end of file diff --git a/src/core/login/lang/en.json b/src/core/login/lang/en.json index 60551f7af..faf05a97a 100644 --- a/src/core/login/lang/en.json +++ b/src/core/login/lang/en.json @@ -58,6 +58,8 @@ "profileinvaliddata": "Invalid value", "potentialidps": "Log in using your account on:", "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/src/core/login/pages/email-signup/email-signup.html b/src/core/login/pages/email-signup/email-signup.html index c3fd44236..afef81dc1 100644 --- a/src/core/login/pages/email-signup/email-signup.html +++ b/src/core/login/pages/email-signup/email-signup.html @@ -82,19 +82,10 @@ - + {{ 'core.login.security_question' | translate }} - - {{ 'core.login.recaptchachallengeimage' | translate }} - - {{ 'core.login.enterthewordsabove' | translate }} - - - - - - {{ 'core.login.getanothercaptcha' | translate }} + @@ -113,7 +104,7 @@ - + diff --git a/src/core/login/pages/email-signup/email-signup.ts b/src/core/login/pages/email-signup/email-signup.ts index b713caab5..4bc221699 100644 --- a/src/core/login/pages/email-signup/email-signup.ts +++ b/src/core/login/pages/email-signup/email-signup.ts @@ -44,6 +44,9 @@ export class CoreLoginEmailSignupPage { countriesKeys: any[]; categories: any[]; settingsLoaded = false; + captcha = { + recaptcharesponse: '' + }; // Validation errors. usernameErrors: any; @@ -98,10 +101,6 @@ export class CoreLoginEmailSignupPage { this.signupForm.addControl(this.settings.namefields[i], this.fb.control('', Validators.required)); } - if (this.settings.recaptchachallengehash && this.settings.recaptchachallengeimage) { - this.signupForm.addControl('recaptcharesponse', this.fb.control('', Validators.required)); - } - if (this.settings.sitepolicy) { this.signupForm.addControl('policyagreed', this.fb.control(false, Validators.requiredTrue)); } @@ -133,8 +132,8 @@ export class CoreLoginEmailSignupPage { this.settings = settings; this.categories = this.loginHelper.formatProfileFieldsForSignup(settings.profilefields); - if (this.signupForm && this.signupForm.controls['recaptcharesponse']) { - this.signupForm.controls['recaptcharesponse'].reset(); // Reset captcha. + if (this.settings.recaptchapublickey) { + this.captcha.recaptcharesponse = ''; // Reset captcha. } this.namefieldsErrors = {}; @@ -183,27 +182,11 @@ export class CoreLoginEmailSignupPage { }); } - /** - * Request another captcha. - * - * @param {boolean} ignoreError Whether to ignore errors. - */ - requestCaptcha(ignoreError?: boolean): void { - const modal = this.domUtils.showModalLoading(); - this.getSignupSettings().catch((err) => { - if (!ignoreError && err) { - this.domUtils.showErrorModal(err); - } - }).finally(() => { - modal.dismiss(); - }); - } - /** * Create account. */ create(): void { - if (!this.signupForm.valid) { + if (!this.signupForm.valid || (this.settings.recaptchapublickey && !this.captcha.recaptcharesponse)) { // Form not valid. Scroll to the first element with errors. if (!this.domUtils.scrollToInputError(this.content)) { // Input not found, show an error modal. @@ -226,9 +209,9 @@ export class CoreLoginEmailSignupPage { params.redirect = this.loginHelper.prepareForSSOLogin(this.siteUrl, service, this.siteConfig.launchurl); } - if (this.settings.recaptchachallengehash && this.settings.recaptchachallengeimage) { - params.recaptchachallengehash = this.settings.recaptchachallengehash; - params.recaptcharesponse = this.signupForm.value.recaptcharesponse; + // Get the recaptcha response (if needed). + if (this.settings.recaptchapublickey && this.captcha.recaptcharesponse) { + params.recaptcharesponse = this.captcha.recaptcharesponse; } // Get the data for the custom profile fields. @@ -243,17 +226,20 @@ export class CoreLoginEmailSignupPage { this.domUtils.showAlert(this.translate.instant('core.success'), message); this.navCtrl.pop(); } else { - this.domUtils.showErrorModalFirstWarning(result.warnings, 'core.login.usernotaddederror', true); + if (result.warnings && result.warnings.length) { + let error = result.warnings[0].message; + if (error == 'incorrect-captcha-sol') { + error = this.translate.instant('mm.login.recaptchaincorrect'); + } - // Error sending, request another capctha since the current one is probably invalid now. - this.requestCaptcha(true); + this.domUtils.showErrorModal(error); + } else { + this.domUtils.showErrorModal('core.login.usernotaddederror', true); + } } }); }).catch((error) => { this.domUtils.showErrorModalDefault(error && error.error, 'core.login.usernotaddederror', true); - - // Error sending, request another capctha since the current one is probably invalid now. - this.requestCaptcha(true); }).finally(() => { modal.dismiss(); }); diff --git a/src/core/user/providers/user-profile-field-delegate.ts b/src/core/user/providers/user-profile-field-delegate.ts index 0e48f4a0c..e047100bd 100644 --- a/src/core/user/providers/user-profile-field-delegate.ts +++ b/src/core/user/providers/user-profile-field-delegate.ts @@ -143,6 +143,10 @@ export class CoreUserProfileFieldDelegate extends CoreDelegate { const result = [], promises = []; + if (!fields) { + return Promise.resolve([]); + } + fields.forEach((field) => { promises.push(this.getDataForField(field, signup, registerAuth, formValues).then((data) => { if (data) { diff --git a/src/lang/en.json b/src/lang/en.json index cf901b5b4..2a0e60e40 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -3,6 +3,8 @@ "add": "Add", "allparticipants": "All participants", "android": "Android", + "answer": "Answer", + "answered": "Answered", "areyousure": "Are you sure?", "back": "Back", "cancel": "Cancel",