MOBILE-3691 recaptcha: Fix recaptcha in Ionic 5
This commit is contained in:
parent
972e1e2a81
commit
e59d180971
@ -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,
|
|
||||||
}, '*');
|
|
||||||
};
|
|
||||||
})();
|
|
@ -44,7 +44,6 @@ import { CoreNavBarButtonsComponent } from './navbar-buttons/navbar-buttons';
|
|||||||
import { CoreNavigationBarComponent } from './navigation-bar/navigation-bar';
|
import { CoreNavigationBarComponent } from './navigation-bar/navigation-bar';
|
||||||
import { CoreProgressBarComponent } from './progress-bar/progress-bar';
|
import { CoreProgressBarComponent } from './progress-bar/progress-bar';
|
||||||
import { CoreRecaptchaComponent } from './recaptcha/recaptcha';
|
import { CoreRecaptchaComponent } from './recaptcha/recaptcha';
|
||||||
import { CoreRecaptchaModalComponent } from './recaptcha/recaptcha-modal';
|
|
||||||
import { CoreSendMessageFormComponent } from './send-message-form/send-message-form';
|
import { CoreSendMessageFormComponent } from './send-message-form/send-message-form';
|
||||||
import { CoreShowPasswordComponent } from './show-password/show-password';
|
import { CoreShowPasswordComponent } from './show-password/show-password';
|
||||||
import { CoreSitePickerComponent } from './site-picker/site-picker';
|
import { CoreSitePickerComponent } from './site-picker/site-picker';
|
||||||
@ -84,7 +83,6 @@ import { CoreHorizontalScrollControlsComponent } from './horizontal-scroll-contr
|
|||||||
CoreNavigationBarComponent,
|
CoreNavigationBarComponent,
|
||||||
CoreProgressBarComponent,
|
CoreProgressBarComponent,
|
||||||
CoreRecaptchaComponent,
|
CoreRecaptchaComponent,
|
||||||
CoreRecaptchaModalComponent,
|
|
||||||
CoreSendMessageFormComponent,
|
CoreSendMessageFormComponent,
|
||||||
CoreShowPasswordComponent,
|
CoreShowPasswordComponent,
|
||||||
CoreSitePickerComponent,
|
CoreSitePickerComponent,
|
||||||
@ -131,7 +129,6 @@ import { CoreHorizontalScrollControlsComponent } from './horizontal-scroll-contr
|
|||||||
CoreNavigationBarComponent,
|
CoreNavigationBarComponent,
|
||||||
CoreProgressBarComponent,
|
CoreProgressBarComponent,
|
||||||
CoreRecaptchaComponent,
|
CoreRecaptchaComponent,
|
||||||
CoreRecaptchaModalComponent,
|
|
||||||
CoreSendMessageFormComponent,
|
CoreSendMessageFormComponent,
|
||||||
CoreShowPasswordComponent,
|
CoreShowPasswordComponent,
|
||||||
CoreSitePickerComponent,
|
CoreSitePickerComponent,
|
||||||
|
@ -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>
|
|
@ -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;
|
|
||||||
};
|
|
@ -16,9 +16,8 @@ import { Component, Input, OnInit } from '@angular/core';
|
|||||||
|
|
||||||
import { CoreLang } from '@services/lang';
|
import { CoreLang } from '@services/lang';
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
|
||||||
import { CoreTextUtils } from '@services/utils/text';
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
import { CoreRecaptchaModalComponent, CoreRecaptchaModalReturn } from './recaptcha-modal';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that allows answering a recaptcha.
|
* 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> {
|
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);
|
const src = CoreTextUtils.concatenatePaths(this.siteUrl!, 'webservice/recaptcha.php?lang=' + this.lang);
|
||||||
|
|
||||||
// Modal to answer the recaptcha.
|
const inAppBrowserWindow = CoreUtils.openInApp(src);
|
||||||
// This is because the size of the recaptcha is dynamic, so it could cause problems if it was displayed inline.
|
if (!inAppBrowserWindow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const modalData = await CoreDomUtils.openModal<CoreRecaptchaModalReturn>({
|
// Set the callbacks once the page is loaded.
|
||||||
component: CoreRecaptchaModalComponent,
|
const loadStopSubscription = inAppBrowserWindow.on('loadstop').subscribe(() => {
|
||||||
cssClass: 'core-modal-fullscreen',
|
inAppBrowserWindow.executeScript({
|
||||||
componentProps: {
|
code:
|
||||||
recaptchaUrl: src,
|
'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) {
|
// Listen for events.
|
||||||
this.expired = modalData.expired;
|
const messageSubscription = inAppBrowserWindow.on('message').subscribe((event) => {
|
||||||
this.model![this.modelValueName] = modalData.value;
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -516,14 +516,8 @@ export class CoreIframeUtilsProvider {
|
|||||||
private injectiOSScripts(userScriptWindow: WKUserScriptWindow) {
|
private injectiOSScripts(userScriptWindow: WKUserScriptWindow) {
|
||||||
const wwwPath = CoreFile.getWWWAbsolutePath();
|
const wwwPath = CoreFile.getWWWAbsolutePath();
|
||||||
const linksPath = CoreTextUtils.concatenatePaths(wwwPath, 'assets/js/iframe-treat-links.js');
|
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: 'CoreIframeUtilsLinksScript', file: linksPath });
|
||||||
userScriptWindow.WKUserScript?.addScript({
|
|
||||||
id: 'CoreIframeUtilsRecaptchaScript',
|
|
||||||
file: recaptchaPath,
|
|
||||||
injectionTime: userScriptWindow.WKUserScript?.InjectionTime.END,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle post messages received by iframes.
|
// Handle post messages received by iframes.
|
||||||
window.addEventListener('message', this.handleIframeMessage.bind(this));
|
window.addEventListener('message', this.handleIframeMessage.bind(this));
|
||||||
|
Loading…
x
Reference in New Issue
Block a user