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 { 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, | ||||
|  | ||||
| @ -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 { 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(); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -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)); | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user