MOBILE-3565 login: Implement site policy page

main
Dani Palou 2020-10-28 15:46:53 +01:00
parent 6df1c2109d
commit a39c65801b
9 changed files with 414 additions and 0 deletions

View File

@ -18,6 +18,7 @@ import { IonicModule } from '@ionic/angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreIconComponent } from './icon/icon';
import { CoreIframeComponent } from './iframe/iframe';
import { CoreLoadingComponent } from './loading/loading';
import { CoreShowPasswordComponent } from './show-password/show-password';
import { CoreEmptyBoxComponent } from './empty-box/empty-box';
@ -27,6 +28,7 @@ import { CorePipesModule } from '@app/pipes/pipes.module';
@NgModule({
declarations: [
CoreIconComponent,
CoreIframeComponent,
CoreLoadingComponent,
CoreShowPasswordComponent,
CoreEmptyBoxComponent,
@ -40,6 +42,7 @@ import { CorePipesModule } from '@app/pipes/pipes.module';
],
exports: [
CoreIconComponent,
CoreIframeComponent,
CoreLoadingComponent,
CoreShowPasswordComponent,
CoreEmptyBoxComponent,

View File

@ -0,0 +1,11 @@
<div [class.core-loading-container]="loading || !safeUrl" [ngStyle]="{'width': iframeWidth, 'height': iframeHeight}">
<!-- Don't add the iframe until safeUrl is set, adding an iframe with null as src causes the iframe to load the whole app. -->
<iframe #iframe *ngIf="safeUrl" [hidden]="loading" class="core-iframe"
[ngStyle]="{'width': iframeWidth, 'height': iframeHeight}" [src]="safeUrl"
[attr.allowfullscreen]="allowFullscreen ? 'allowfullscreen' : null">
</iframe>
<span class="core-loading-spinner">
<ion-spinner *ngIf="loading"></ion-spinner>
</span>
</div>

View File

@ -0,0 +1,31 @@
ion-app.app-root core-iframe {
> div {
max-width: 100%;
max-height: 100%;
}
iframe {
border: 0;
display: block;
max-width: 100%;
background-color: $gray-light;
}
.core-loading-container {
position: absolute;
@include position(0, 0, 0, 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;
}
}
}

View File

@ -0,0 +1,117 @@
// (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, Output, ViewChild, ElementRef, EventEmitter, OnChanges, SimpleChange,
} from '@angular/core';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
import { NavController } from '@ionic/angular';
import { CoreFile } from '@services/file';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreUrlUtils } from '@services/utils/url';
import { CoreIframeUtils } from '@services/utils/iframe';
import { CoreUtils } from '@services/utils/utils';
import { CoreLogger } from '@singletons/logger';
@Component({
selector: 'core-iframe',
templateUrl: 'core-iframe.html',
})
export class CoreIframeComponent implements OnChanges {
@ViewChild('iframe') iframe?: ElementRef;
@Input() src?: string;
@Input() iframeWidth?: string;
@Input() iframeHeight?: string;
@Input() allowFullscreen?: boolean | string;
@Output() loaded: EventEmitter<HTMLIFrameElement> = new EventEmitter<HTMLIFrameElement>();
loading?: boolean;
safeUrl?: SafeResourceUrl;
protected readonly IFRAME_TIMEOUT = 15000;
protected logger: CoreLogger;
protected initialized = false;
constructor(
protected sanitizer: DomSanitizer,
protected navCtrl: NavController,
) {
this.logger = CoreLogger.getInstance('CoreIframe');
this.loaded = new EventEmitter<HTMLIFrameElement>();
}
/**
* Init the data.
*/
protected init(): void {
if (this.initialized) {
return;
}
const iframe: HTMLIFrameElement | undefined = this.iframe?.nativeElement;
if (!iframe) {
return;
}
this.initialized = true;
this.iframeWidth = (this.iframeWidth && CoreDomUtils.instance.formatPixelsSize(this.iframeWidth)) || '100%';
this.iframeHeight = (this.iframeHeight && CoreDomUtils.instance.formatPixelsSize(this.iframeHeight)) || '100%';
this.allowFullscreen = CoreUtils.instance.isTrueOrOne(this.allowFullscreen);
// Show loading only with external URLs.
this.loading = !this.src || !CoreUrlUtils.instance.isLocalFileUrl(this.src);
// @todo const navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl;
CoreIframeUtils.instance.treatFrame(iframe, false, this.navCtrl);
iframe.addEventListener('load', () => {
this.loading = false;
this.loaded.emit(iframe); // Notify iframe was loaded.
});
iframe.addEventListener('error', () => {
this.loading = false;
CoreDomUtils.instance.showErrorModal('core.errorloadingcontent', true);
});
if (this.loading) {
setTimeout(() => {
this.loading = false;
}, this.IFRAME_TIMEOUT);
}
}
/**
* Detect changes on input properties.
*/
async ngOnChanges(changes: {[name: string]: SimpleChange }): Promise<void> {
if (changes.src) {
const url = CoreUrlUtils.instance.getYoutubeEmbedUrl(changes.src.currentValue) || changes.src.currentValue;
await CoreIframeUtils.instance.fixIframeCookies(url);
this.safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(CoreFile.instance.convertFileSrc(url));
// Now that the URL has been set, initialize the iframe. Wait for the iframe to the added to the DOM.
setTimeout(() => {
this.init();
});
}
}
}

View File

@ -47,6 +47,10 @@ const routes: Routes = [
loadChildren: () => import('./pages/change-password/change-password.module')
.then( m => m.CoreLoginChangePasswordPageModule),
},
{
path: 'sitepolicy',
loadChildren: () => import('./pages/site-policy/site-policy.module').then( m => m.CoreLoginSitePolicyPageModule),
},
];
@NgModule({

View File

@ -0,0 +1,32 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>{{ 'core.login.policyagreement' | translate }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<core-loading [hideUntil]="policyLoaded">
<ion-list *ngIf="sitePolicy">
<ion-item class="ion-text-wrap">
<ion-label><p>{{ 'core.login.policyagree' | translate }}</p></ion-label>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label><p>
<a [href]="sitePolicy" core-link [capture]="false">{{ 'core.login.policyagreementclick' | translate }}</a>
</p></ion-label>
</ion-item>
<ion-card *ngIf="showInline">
<core-iframe [src]="sitePolicy"></core-iframe>
</ion-card>
<ion-button class="ion-text-wrap ion-margin" expand="block" (click)="accept()">
{{ 'core.login.policyaccept' | translate }}
</ion-button>
<ion-button class="ion-text-wrap ion-margin" expand="block" color="light" (click)="cancel()">
{{ 'core.login.cancel' | translate }}
</ion-button>
</ion-list>
</core-loading>
</ion-content>

View File

@ -0,0 +1,46 @@
// (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 { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule, Routes } from '@angular/router';
import { IonicModule } from '@ionic/angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CoreLoginSitePolicyPage } from './site-policy.page';
const routes: Routes = [
{
path: '',
component: CoreLoginSitePolicyPage,
},
];
@NgModule({
imports: [
RouterModule.forChild(routes),
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule,
],
declarations: [
CoreLoginSitePolicyPage,
],
exports: [RouterModule],
})
export class CoreLoginSitePolicyPageModule {}

View File

@ -0,0 +1,139 @@
// (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, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { NavController } from '@ionic/angular';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils';
import { CoreMimetypeUtils } from '@services/utils/mimetype';
import { CoreLoginHelper } from '@core/login/services/helper';
import { CoreSite } from '@classes/site';
/**
* Page to accept a site policy.
*/
@Component({
selector: 'page-core-login-site-policy',
templateUrl: 'site-policy.html',
})
export class CoreLoginSitePolicyPage implements OnInit {
sitePolicy?: string;
showInline?: boolean;
policyLoaded?: boolean;
protected siteId?: string;
protected currentSite?: CoreSite;
constructor(
protected navCtrl: NavController,
protected route: ActivatedRoute,
) {
}
/**
* Component initialized.
*/
ngOnInit(): void {
const params = this.route.snapshot.queryParams;
this.siteId = params['siteId'];
this.currentSite = CoreSites.instance.getCurrentSite();
if (!this.currentSite) {
// Not logged in, stop.
this.cancel();
return;
}
const currentSiteId = this.currentSite.id;
this.siteId = this.siteId || currentSiteId;
if (this.siteId != currentSiteId || !this.currentSite.wsAvailable('core_user_agree_site_policy')) {
// Not current site or WS not available, stop.
this.cancel();
return;
}
this.fetchSitePolicy();
}
/**
* Fetch the site policy URL.
*
* @return Promise resolved when done.
*/
protected async fetchSitePolicy(): Promise<void> {
try {
this.sitePolicy = await CoreLoginHelper.instance.getSitePolicy(this.siteId);
} catch (error) {
CoreDomUtils.instance.showErrorModalDefault(error, 'Error getting site policy.');
this.cancel();
return;
}
// Try to get the mime type.
try {
const mimeType = await CoreUtils.instance.getMimeTypeFromUrl(this.sitePolicy);
const extension = CoreMimetypeUtils.instance.getExtension(mimeType, this.sitePolicy);
this.showInline = extension == 'html' || extension == 'htm';
} catch (error) {
// Unable to get mime type, assume it's not supported.
this.showInline = false;
} finally {
this.policyLoaded = true;
}
}
/**
* Cancel.
*
* @return Promise resolved when done.
*/
async cancel(): Promise<void> {
await CoreUtils.instance.ignoreErrors(CoreSites.instance.logout());
await this.navCtrl.navigateRoot('/login/sites');
}
/**
* Accept the site policy.
*
* @return Promise resolved when done.
*/
async accept(): Promise<void> {
const modal = await CoreDomUtils.instance.showModalLoading('core.sending', true);
try {
await CoreLoginHelper.instance.acceptSitePolicy(this.siteId);
// Success accepting, go to site initial page.
// Invalidate cache since some WS don't return error if site policy is not accepted.
await CoreUtils.instance.ignoreErrors(this.currentSite!.invalidateWsCache());
await CoreLoginHelper.instance.goToSiteInitialPage();
} catch (error) {
CoreDomUtils.instance.showErrorModalDefault(error, 'Error accepting site policy.');
} finally {
modal.dismiss();
}
}
}

View File

@ -15,6 +15,7 @@
import { Injectable } from '@angular/core';
import { NavController } from '@ionic/angular';
import { WKUserScriptWindow, WKUserScriptInjectionTime } from 'cordova-plugin-wkuserscript';
import { WKWebViewCookiesWindow } from 'cordova-plugin-wkwebview-cookies';
import { CoreApp } from '@services/app';
import { CoreFile } from '@services/file';
@ -476,6 +477,36 @@ export class CoreIframeUtilsProvider {
window.addEventListener('message', this.handleIframeMessage.bind(this));
}
/**
* Fix cookies for an iframe URL.
*
* @param url URL of the iframe.
* @return Promise resolved when done.
*/
async fixIframeCookies(url: string): Promise<void> {
if (!CoreApp.instance.isIOS() || !url || CoreUrlUtils.instance.isLocalFileUrl(url)) {
// No need to fix cookies.
return;
}
// Save a "fake" cookie for the iframe's domain to fix a bug in WKWebView.
try {
const win = <WKWebViewCookiesWindow> window;
const urlParts = CoreUrl.parse(url);
if (urlParts?.domain && win.WKWebViewCookies) {
await win.WKWebViewCookies.setCookie({
name: 'MoodleAppCookieForWKWebView',
value: '1',
domain: urlParts.domain,
});
}
} catch (err) {
// Ignore errors.
this.logger.error('Error setting cookie', err);
}
}
}
export class CoreIframeUtils extends makeSingleton(CoreIframeUtilsProvider) {}