MOBILE-3565 login: Implement site policy page
parent
6df1c2109d
commit
a39c65801b
|
@ -18,6 +18,7 @@ import { IonicModule } from '@ionic/angular';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
import { CoreIconComponent } from './icon/icon';
|
import { CoreIconComponent } from './icon/icon';
|
||||||
|
import { CoreIframeComponent } from './iframe/iframe';
|
||||||
import { CoreLoadingComponent } from './loading/loading';
|
import { CoreLoadingComponent } from './loading/loading';
|
||||||
import { CoreShowPasswordComponent } from './show-password/show-password';
|
import { CoreShowPasswordComponent } from './show-password/show-password';
|
||||||
import { CoreEmptyBoxComponent } from './empty-box/empty-box';
|
import { CoreEmptyBoxComponent } from './empty-box/empty-box';
|
||||||
|
@ -27,6 +28,7 @@ import { CorePipesModule } from '@app/pipes/pipes.module';
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
CoreIconComponent,
|
CoreIconComponent,
|
||||||
|
CoreIframeComponent,
|
||||||
CoreLoadingComponent,
|
CoreLoadingComponent,
|
||||||
CoreShowPasswordComponent,
|
CoreShowPasswordComponent,
|
||||||
CoreEmptyBoxComponent,
|
CoreEmptyBoxComponent,
|
||||||
|
@ -40,6 +42,7 @@ import { CorePipesModule } from '@app/pipes/pipes.module';
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
CoreIconComponent,
|
CoreIconComponent,
|
||||||
|
CoreIframeComponent,
|
||||||
CoreLoadingComponent,
|
CoreLoadingComponent,
|
||||||
CoreShowPasswordComponent,
|
CoreShowPasswordComponent,
|
||||||
CoreEmptyBoxComponent,
|
CoreEmptyBoxComponent,
|
||||||
|
|
|
@ -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>
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -47,6 +47,10 @@ const routes: Routes = [
|
||||||
loadChildren: () => import('./pages/change-password/change-password.module')
|
loadChildren: () => import('./pages/change-password/change-password.module')
|
||||||
.then( m => m.CoreLoginChangePasswordPageModule),
|
.then( m => m.CoreLoginChangePasswordPageModule),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'sitepolicy',
|
||||||
|
loadChildren: () => import('./pages/site-policy/site-policy.module').then( m => m.CoreLoginSitePolicyPageModule),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
|
|
@ -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>
|
|
@ -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 {}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -15,6 +15,7 @@
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { NavController } from '@ionic/angular';
|
import { NavController } from '@ionic/angular';
|
||||||
import { WKUserScriptWindow, WKUserScriptInjectionTime } from 'cordova-plugin-wkuserscript';
|
import { WKUserScriptWindow, WKUserScriptInjectionTime } from 'cordova-plugin-wkuserscript';
|
||||||
|
import { WKWebViewCookiesWindow } from 'cordova-plugin-wkwebview-cookies';
|
||||||
|
|
||||||
import { CoreApp } from '@services/app';
|
import { CoreApp } from '@services/app';
|
||||||
import { CoreFile } from '@services/file';
|
import { CoreFile } from '@services/file';
|
||||||
|
@ -476,6 +477,36 @@ export class CoreIframeUtilsProvider {
|
||||||
window.addEventListener('message', this.handleIframeMessage.bind(this));
|
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) {}
|
export class CoreIframeUtils extends makeSingleton(CoreIframeUtilsProvider) {}
|
||||||
|
|
Loading…
Reference in New Issue