MOBILE-3691 recaptcha: Fix recaptcha in Ionic 5

main
Dani Palou 2021-05-18 12:47:37 +02:00
parent 972e1e2a81
commit e59d180971
6 changed files with 37 additions and 207 deletions

View File

@ -1,42 +0,0 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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.
(function () {
var url = location.href;
if (!url.match(/^https?:\/\//i) || !url.match(/\/webservice\/recaptcha\.php/i)) {
// Not the recaptcha script, stop.
return;
}
// Define recaptcha callbacks.
window.recaptchacallback = function(value) {
window.parent.postMessage({
environment: 'moodleapp',
context: 'recaptcha',
action: 'callback',
frameUrl: location.href,
value: value,
}, '*');
};
window.recaptchaexpiredcallback = function() {
window.parent.postMessage({
environment: 'moodleapp',
context: 'recaptcha',
action: 'expired',
frameUrl: location.href,
}, '*');
};
})();

View File

@ -44,7 +44,6 @@ import { CoreNavBarButtonsComponent } from './navbar-buttons/navbar-buttons';
import { CoreNavigationBarComponent } from './navigation-bar/navigation-bar';
import { CoreProgressBarComponent } from './progress-bar/progress-bar';
import { CoreRecaptchaComponent } from './recaptcha/recaptcha';
import { CoreRecaptchaModalComponent } from './recaptcha/recaptcha-modal';
import { CoreSendMessageFormComponent } from './send-message-form/send-message-form';
import { CoreShowPasswordComponent } from './show-password/show-password';
import { CoreSitePickerComponent } from './site-picker/site-picker';
@ -84,7 +83,6 @@ import { CoreHorizontalScrollControlsComponent } from './horizontal-scroll-contr
CoreNavigationBarComponent,
CoreProgressBarComponent,
CoreRecaptchaComponent,
CoreRecaptchaModalComponent,
CoreSendMessageFormComponent,
CoreShowPasswordComponent,
CoreSitePickerComponent,
@ -131,7 +129,6 @@ import { CoreHorizontalScrollControlsComponent } from './horizontal-scroll-contr
CoreNavigationBarComponent,
CoreProgressBarComponent,
CoreRecaptchaComponent,
CoreRecaptchaModalComponent,
CoreSendMessageFormComponent,
CoreShowPasswordComponent,
CoreSitePickerComponent,

View File

@ -1,14 +0,0 @@
<ion-header>
<ion-toolbar>
<ion-title>{{ 'core.login.security_question' | translate }}</ion-title>
<ion-buttons slot="end">
<ion-button fill="clear" (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
<ion-icon slot="icon-only" name="fas-times" aria-hidden="true"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<core-iframe [src]="recaptchaUrl" (loaded)="loaded($event)"></core-iframe>
</ion-content>

View File

@ -1,126 +0,0 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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, OnDestroy } from '@angular/core';
import { ModalController } from '@singletons';
/**
* Component to display a the recaptcha in a modal.
*/
@Component({
selector: 'core-recaptcha-modal',
templateUrl: 'core-recaptcha-modal.html',
})
export class CoreRecaptchaModalComponent implements OnDestroy {
@Input() recaptchaUrl?: string;
expired = false;
value = '';
protected messageListenerFunction: (event: MessageEvent) => Promise<void>;
constructor() {
// Listen for messages from the iframe.
this.messageListenerFunction = this.onIframeMessage.bind(this);
window.addEventListener('message', this.messageListenerFunction);
}
/**
* Close modal.
*/
closeModal(): void {
ModalController.dismiss(<CoreRecaptchaModalReturn>{
expired: this.expired,
value: this.value,
});
}
/**
* The iframe with the recaptcha was loaded.
*
* @param iframe Iframe element.
*/
loaded(iframe: HTMLIFrameElement): void {
// Search the iframe content.
const contentWindow = iframe?.contentWindow;
if (contentWindow) {
try {
// Set the callbacks we're interested in.
contentWindow['recaptchacallback'] = this.onRecaptchaCallback.bind(this);
contentWindow['recaptchaexpiredcallback'] = this.onRecaptchaExpiredCallback.bind(this);
} catch (error) {
// Cannot access the window.
}
}
}
/**
* Treat an iframe message event.
*
* @param event Event.
* @return Promise resolved when done.
*/
protected async onIframeMessage(event: MessageEvent): Promise<void> {
if (!event.data || event.data.environment != 'moodleapp' || event.data.context != 'recaptcha') {
return;
}
switch (event.data.action) {
case 'callback':
this.onRecaptchaCallback(event.data.value);
break;
case 'expired':
this.onRecaptchaExpiredCallback();
break;
default:
break;
}
}
/**
* Recapcha callback called.
*
* @param value Value received.
*/
protected onRecaptchaCallback(value: string): void {
this.expired = false;
this.value = value;
this.closeModal();
}
/**
* Recapcha expired callback called.
*/
protected onRecaptchaExpiredCallback(): void {
this.expired = true;
this.value = '';
}
/**
* Component destroyed.
*/
ngOnDestroy(): void {
window.removeEventListener('message', this.messageListenerFunction);
}
}
export type CoreRecaptchaModalReturn = {
expired: boolean;
value: string;
};

View File

@ -16,9 +16,8 @@ import { Component, Input, OnInit } from '@angular/core';
import { CoreLang } from '@services/lang';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreTextUtils } from '@services/utils/text';
import { CoreRecaptchaModalComponent, CoreRecaptchaModalReturn } from './recaptcha-modal';
import { CoreUtils } from '@services/utils/utils';
/**
* Component that allows answering a recaptcha.
@ -57,27 +56,49 @@ export class CoreRecaptchaComponent implements OnInit {
}
/**
* Open the recaptcha modal.
* Let the user answer the recaptcha.
*/
async answerRecaptcha(): Promise<void> {
// Set the iframe src. We use an iframe because reCaptcha V2 doesn't work with file:// protocol.
// Open the recaptcha challenge in an InAppBrowser.
// The app used to use an iframe for this, but the app can no longer access the iframe to create the required callbacks.
// The app cannot render the recaptcha directly because it has problems with the local protocols and domains.
const src = CoreTextUtils.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 inAppBrowserWindow = CoreUtils.openInApp(src);
if (!inAppBrowserWindow) {
return;
}
const modalData = await CoreDomUtils.openModal<CoreRecaptchaModalReturn>({
component: CoreRecaptchaModalComponent,
cssClass: 'core-modal-fullscreen',
componentProps: {
recaptchaUrl: src,
},
// Set the callbacks once the page is loaded.
const loadStopSubscription = inAppBrowserWindow.on('loadstop').subscribe(() => {
inAppBrowserWindow.executeScript({
code:
'window.recaptchacallback = (value) => webkit.messageHandlers.cordova_iab.postMessage(' +
'JSON.stringify({ action: "callback", value }));' +
'window.recaptchaexpiredcallback = () => webkit.messageHandlers.cordova_iab.postMessage(' +
'JSON.stringify({ action: "expired" }));',
});
});
if (modalData) {
this.expired = modalData.expired;
this.model![this.modelValueName] = modalData.value;
}
// Listen for events.
const messageSubscription = inAppBrowserWindow.on('message').subscribe((event) => {
if (!event.data) {
return;
}
if (event.data.action == 'expired') {
this.expired = true;
this.model![this.modelValueName] = '';
} else if (event.data.action == 'callback') {
this.expired = false;
this.model![this.modelValueName] = event.data.value;
// Close the InAppBrowser now.
inAppBrowserWindow.close();
messageSubscription.unsubscribe();
loadStopSubscription.unsubscribe();
}
});
}
}

View File

@ -516,14 +516,8 @@ export class CoreIframeUtilsProvider {
private injectiOSScripts(userScriptWindow: WKUserScriptWindow) {
const wwwPath = CoreFile.getWWWAbsolutePath();
const linksPath = CoreTextUtils.concatenatePaths(wwwPath, 'assets/js/iframe-treat-links.js');
const recaptchaPath = CoreTextUtils.concatenatePaths(wwwPath, 'assets/js/iframe-recaptcha.js');
userScriptWindow.WKUserScript?.addScript({ id: 'CoreIframeUtilsLinksScript', file: linksPath });
userScriptWindow.WKUserScript?.addScript({
id: 'CoreIframeUtilsRecaptchaScript',
file: recaptchaPath,
injectionTime: userScriptWindow.WKUserScript?.InjectionTime.END,
});
// Handle post messages received by iframes.
window.addEventListener('message', this.handleIframeMessage.bind(this));