From 7893d718a2acc896099c90683490ac6d06ccdc09 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 1 Dec 2017 08:51:07 +0100 Subject: [PATCH] MOBILE-2253 login: Implement site policy page --- src/classes/sqlitedb.ts | 1 - src/components/components.module.ts | 7 +- src/components/iframe/iframe.html | 4 + src/components/iframe/iframe.scss | 5 + src/components/iframe/iframe.ts | 260 ++++++++++++++++++ .../login/pages/site-policy/site-policy.html | 24 ++ .../pages/site-policy/site-policy.module.ts | 35 +++ .../login/pages/site-policy/site-policy.scss | 5 + .../login/pages/site-policy/site-policy.ts | 121 ++++++++ src/core/login/providers/helper.ts | 12 +- src/providers/app.ts | 7 +- src/providers/logger.ts | 2 +- 12 files changed, 473 insertions(+), 10 deletions(-) create mode 100644 src/components/iframe/iframe.html create mode 100644 src/components/iframe/iframe.scss create mode 100644 src/components/iframe/iframe.ts create mode 100644 src/core/login/pages/site-policy/site-policy.html create mode 100644 src/core/login/pages/site-policy/site-policy.module.ts create mode 100644 src/core/login/pages/site-policy/site-policy.scss create mode 100644 src/core/login/pages/site-policy/site-policy.ts diff --git a/src/classes/sqlitedb.ts b/src/classes/sqlitedb.ts index d2e88d5d2..6b718eca8 100644 --- a/src/classes/sqlitedb.ts +++ b/src/classes/sqlitedb.ts @@ -298,7 +298,6 @@ export class SQLiteDB { } return this.execute(`DELETE FROM ${table} ${select}`, params); - } /** diff --git a/src/components/components.module.ts b/src/components/components.module.ts index 781ea29ba..33b38d327 100644 --- a/src/components/components.module.ts +++ b/src/components/components.module.ts @@ -20,13 +20,15 @@ import { CoreLoadingComponent } from './loading/loading'; import { CoreMarkRequiredComponent } from './mark-required/mark-required'; import { CoreInputErrorsComponent } from './input-errors/input-errors'; import { CoreShowPasswordComponent } from './show-password/show-password'; +import { CoreIframeComponent } from './iframe/iframe'; @NgModule({ declarations: [ CoreLoadingComponent, CoreMarkRequiredComponent, CoreInputErrorsComponent, - CoreShowPasswordComponent + CoreShowPasswordComponent, + CoreIframeComponent ], imports: [ IonicModule, @@ -37,7 +39,8 @@ import { CoreShowPasswordComponent } from './show-password/show-password'; CoreLoadingComponent, CoreMarkRequiredComponent, CoreInputErrorsComponent, - CoreShowPasswordComponent + CoreShowPasswordComponent, + CoreIframeComponent ] }) export class CoreComponentsModule {} diff --git a/src/components/iframe/iframe.html b/src/components/iframe/iframe.html new file mode 100644 index 000000000..75cea2664 --- /dev/null +++ b/src/components/iframe/iframe.html @@ -0,0 +1,4 @@ +
+ + +
\ No newline at end of file diff --git a/src/components/iframe/iframe.scss b/src/components/iframe/iframe.scss new file mode 100644 index 000000000..12c75ceb3 --- /dev/null +++ b/src/components/iframe/iframe.scss @@ -0,0 +1,5 @@ +core-iframe { + > div { + height: 100%; + } +} diff --git a/src/components/iframe/iframe.ts b/src/components/iframe/iframe.ts new file mode 100644 index 000000000..6658ffa31 --- /dev/null +++ b/src/components/iframe/iframe.ts @@ -0,0 +1,260 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, OnInit, ViewChild, ElementRef } from '@angular/core'; +import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; +import { Platform } from 'ionic-angular'; +import { CoreFileProvider } from '../../providers/file'; +import { CoreLoggerProvider } from '../../providers/logger'; +import { CoreSitesProvider } from '../../providers/sites'; +import { CoreDomUtilsProvider } from '../../providers/utils/dom'; +import { CoreTextUtilsProvider } from '../../providers/utils/text'; +import { CoreUrlUtilsProvider } from '../../providers/utils/url'; +import { CoreUtilsProvider } from '../../providers/utils/utils'; + +/** + */ +@Component({ + selector: 'core-iframe', + templateUrl: 'iframe.html' +}) +export class CoreIframeComponent implements OnInit { + + @ViewChild('iframe') iframe: ElementRef; + @Input() src: string; + @Input() iframeWidth: string; + @Input() iframeHeight: string; + loading: boolean; + safeUrl: SafeResourceUrl; + + protected logger; + protected tags = ['iframe', 'frame', 'object', 'embed']; + protected IFRAME_TIMEOUT = 15000; + + constructor(logger: CoreLoggerProvider, private fileProvider: CoreFileProvider, private urlUtils: CoreUrlUtilsProvider, + private textUtils: CoreTextUtilsProvider, private utils: CoreUtilsProvider, private domUtils: CoreDomUtilsProvider, + private sitesProvider: CoreSitesProvider, private platform: Platform, private sanitizer: DomSanitizer) { + this.logger = logger.getInstance('CoreIframe'); + } + + /** + * Component being initialized. + */ + ngOnInit() { + let iframe: HTMLIFrameElement = this.iframe && this.iframe.nativeElement; + + this.safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(this.src); + this.iframeWidth = this.domUtils.formatPixelsSize(this.iframeWidth) || '100%'; + this.iframeHeight = this.domUtils.formatPixelsSize(this.iframeHeight) || '100%'; + + // Show loading only with external URLs. + this.loading = !!this.src.match(/^https?:\/\//i); + + this.treatFrame(iframe); + + if (this.loading) { + iframe.addEventListener('load', () => { + this.loading = false; + }); + + iframe.addEventListener('error', () => { + this.loading = false; + this.domUtils.showErrorModal('mm.core.errorloadingcontent', true); + }); + + setTimeout(() => { + this.loading = false; + }, this.IFRAME_TIMEOUT); + } + } + + /** + * Given an element, return the content window and document. + * + * @param {any} element Element to treat. + * @return {{ window: Window, document: Document }} Window and Document. + */ + protected getContentWindowAndDocument(element: any) : { window: Window, document: Document } { + let contentWindow: Window = element.contentWindow, + contentDocument: Document = element.contentDocument || (contentWindow && contentWindow.document); + + if (!contentWindow && contentDocument) { + // It's probably an . Try to get the window. + contentWindow = contentDocument.defaultView; + } + + if (!contentWindow && element.getSVGDocument) { + // It's probably an . Try to get the window and the document. + contentDocument = element.getSVGDocument(); + if (contentDocument && contentDocument.defaultView) { + contentWindow = contentDocument.defaultView; + } else if (element.window) { + contentWindow = element.window; + } else if (element.getWindow) { + contentWindow = element.getWindow(); + } + } + + return {window: contentWindow, document: contentDocument}; + } + + /** + * Intercept window.open in a frame and its subframes, shows an error modal instead. + * Search links () and open them in browser or InAppBrowser if needed. + * + * @param {any} element Element to treat. + */ + protected treatFrame(element: any) : void { + if (element) { + let winAndDoc = this.getContentWindowAndDocument(element); + // Redefine window.open in this element and sub frames, it might have been loaded already. + this.redefineWindowOpen(element, winAndDoc.window, winAndDoc.document); + // Treat links. + this.treatLinks(element, winAndDoc.document); + + element.addEventListener('load', () => { + // Element loaded, redefine window.open and treat links again. + winAndDoc = this.getContentWindowAndDocument(element); + this.redefineWindowOpen(element, winAndDoc.window, winAndDoc.document); + this.treatLinks(element, winAndDoc.document); + }); + } + } + + /** + * Redefine the open method in the contentWindow of an element and the sub frames. + * + * @param {any} element Element to treat. + * @param {Window} contentWindow The window of the element contents. + * @param {Document} contentDocument The document of the element contents. + */ + protected redefineWindowOpen(element: any, contentWindow: Window, contentDocument: Document) : void { + if (contentWindow) { + // Intercept window.open. + contentWindow.open = (url: string) : Window => { + const scheme = this.urlUtils.getUrlScheme(url); + if (!scheme) { + // It's a relative URL, use the frame src to create the full URL. + const src = element.src || element.data; + if (src) { + const dirAndFile = this.fileProvider.getFileAndDirectoryFromPath(src); + if (dirAndFile.directory) { + url = this.textUtils.concatenatePaths(dirAndFile.directory, url); + } else { + this.logger.warn('Cannot get iframe dir path to open relative url', url, element); + return new Window(); // Return new Window object. + } + } else { + this.logger.warn('Cannot get iframe src to open relative url', url, element); + return new Window(); // Return new Window object. + } + } + + if (url.indexOf('cdvfile://') === 0 || url.indexOf('file://') === 0) { + // It's a local file. + this.utils.openFile(url).catch((error) => { + this.domUtils.showErrorModal(error); + }); + } else { + // It's an external link, we will open with browser. Check if we need to auto-login. + if (!this.sitesProvider.isLoggedIn()) { + // Not logged in, cannot auto-login. + this.utils.openInBrowser(url); + } else { + this.sitesProvider.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(url); + } + } + + return new Window(); // Return new Window object. + }; + } + + if (contentDocument) { + // Search sub frames. + this.tags.forEach((tag) => { + const elements = Array.from(contentDocument.querySelectorAll(tag)); + elements.forEach((subElement) => { + this.treatFrame(subElement); + }); + }); + } + } + + /** + * Search links () and open them in browser or InAppBrowser if needed. + * Only links that haven't been treated by the iframe's Javascript will be treated. + * + * @param {any} element Element to treat. + * @param {Document} contentDocument The document of the element contents. + */ + protected treatLinks(element: any, contentDocument: Document) : void { + if (!contentDocument) { + return; + } + + const links = Array.from(contentDocument.querySelectorAll('a')); + links.forEach((el: HTMLAnchorElement) => { + const href = el.href; + + // Check that href is not null. + if (href) { + const scheme = this.urlUtils.getUrlScheme(href); + if (scheme && scheme == 'javascript') { + // Javascript links should be treated by the iframe's Javascript. + // There's nothing to be done with these links, so they'll be ignored. + return; + } else if (scheme && scheme != 'file' && scheme != 'filesystem') { + // Scheme suggests it's an external resource, open it in browser. + el.addEventListener('click', (e) => { + // If the link's already prevented by SCORM JS then we won't open it in browser. + if (!e.defaultPrevented) { + e.preventDefault(); + if (!this.sitesProvider.isLoggedIn()) { + this.utils.openInBrowser(href); + } else { + this.sitesProvider.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(href); + } + } + }); + } else if (el.target == '_parent' || el.target == '_top' || el.target == '_blank') { + // Opening links with _parent, _top or _blank can break the app. We'll open it in InAppBrowser. + el.addEventListener('click', (e) => { + // If the link's already prevented by SCORM JS then we won't open it in InAppBrowser. + if (!e.defaultPrevented) { + e.preventDefault(); + this.utils.openFile(href).catch((error) => { + this.domUtils.showErrorModal(error); + }); + } + }); + } else if (this.platform.is('ios') && (!el.target || el.target == '_self')) { + // In cordova ios 4.1.0 links inside iframes stopped working. We'll manually treat them. + el.addEventListener('click', (e) => { + // If the link's already prevented by SCORM JS then we won't treat it. + if (!e.defaultPrevented) { + if (element.tagName.toLowerCase() == 'object') { + e.preventDefault(); + element.attr('data', href); + } else { + e.preventDefault(); + element.attr('src', href); + } + } + }); + } + } + }); + } + +} diff --git a/src/core/login/pages/site-policy/site-policy.html b/src/core/login/pages/site-policy/site-policy.html new file mode 100644 index 000000000..dbcafdfd4 --- /dev/null +++ b/src/core/login/pages/site-policy/site-policy.html @@ -0,0 +1,24 @@ + + + {{ 'mm.login.policyagreement' | translate }} + + + + + + + {{ 'mm.login.policyagree' | translate }} + + +

{{ 'mm.login.policyagreementclick' | translate }}

+ + + + + + + + + + + diff --git a/src/core/login/pages/site-policy/site-policy.module.ts b/src/core/login/pages/site-policy/site-policy.module.ts new file mode 100644 index 000000000..27168a1ff --- /dev/null +++ b/src/core/login/pages/site-policy/site-policy.module.ts @@ -0,0 +1,35 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { IonicPageModule } from 'ionic-angular'; +import { CoreLoginSitePolicyPage } from './site-policy'; +import { CoreLoginModule } from '../../login.module'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '../../../../components/components.module'; +import { CoreDirectivesModule } from '../../../../directives/directives.module'; + +@NgModule({ + declarations: [ + CoreLoginSitePolicyPage + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + CoreLoginModule, + IonicPageModule.forChild(CoreLoginSitePolicyPage), + TranslateModule.forChild() + ] +}) +export class CoreLoginSitePolicyPageModule {} diff --git a/src/core/login/pages/site-policy/site-policy.scss b/src/core/login/pages/site-policy/site-policy.scss new file mode 100644 index 000000000..d87f901c2 --- /dev/null +++ b/src/core/login/pages/site-policy/site-policy.scss @@ -0,0 +1,5 @@ +page-core-login-site-policy { + .card { + height: 300px; + } +} diff --git a/src/core/login/pages/site-policy/site-policy.ts b/src/core/login/pages/site-policy/site-policy.ts new file mode 100644 index 000000000..1c051b005 --- /dev/null +++ b/src/core/login/pages/site-policy/site-policy.ts @@ -0,0 +1,121 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 } from '@angular/core'; +import { IonicPage, NavController, NavParams } from 'ionic-angular'; +import { CoreSitesProvider } from '../../../../providers/sites'; +import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; +import { CoreMimetypeUtilsProvider } from '../../../../providers/utils/mimetype'; +import { CoreLoginHelperProvider } from '../../providers/helper'; +import { CoreSite } from '../../../../classes/site'; + +/** + * Page to accept a site policy. + */ +@IonicPage() +@Component({ + selector: 'page-core-login-site-policy', + templateUrl: 'site-policy.html', +}) +export class CoreLoginSitePolicyPage { + sitePolicy: string; + showInline: boolean; + policyLoaded: boolean; + protected siteId: string; + protected currentSite: CoreSite; + + constructor(private navCtrl: NavController, navParams: NavParams, private loginHelper: CoreLoginHelperProvider, + private domUtils: CoreDomUtilsProvider, private sitesProvider: CoreSitesProvider, + private mimeUtils: CoreMimetypeUtilsProvider) { + this.siteId = navParams.get('siteId'); + } + + /** + * View laoded. + */ + ionViewDidLoad() { + this.currentSite = this.sitesProvider.getCurrentSite(); + + if (!this.currentSite) { + // Not logged in, stop. + this.cancel(); + return; + } + + let 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. + */ + protected fetchSitePolicy() { + return this.loginHelper.getSitePolicy(this.siteId).then((sitePolicy) => { + this.sitePolicy = sitePolicy; + + // Try to get the mime type. + return this.mimeUtils.getMimeTypeFromUrl(sitePolicy).then((mimeType) => { + const extension = this.mimeUtils.getExtension(mimeType, sitePolicy); + this.showInline = extension == 'html' || extension == 'htm'; + }).catch(() => { + // Unable to get mime type, assume it's not supported. + this.showInline = false; + }).finally(() => { + this.policyLoaded = true; + }); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error && error.error, 'Error getting site policy.'); + this.cancel(); + }); + } + + /** + * Cancel. + */ + cancel() : void { + this.sitesProvider.logout().catch(() => { + // Ignore errors, shouldn't happen. + }).then(() => { + this.navCtrl.setRoot('CoreLoginSitesPage'); + }); + } + + /** + * Accept the site policy. + */ + accept() : void { + let modal = this.domUtils.showModalLoading('mm.core.sending', true); + this.loginHelper.acceptSitePolicy(this.siteId).then(() => { + // Success accepting, go to site initial page. + // Invalidate cache since some WS don't return error if site policy is not accepted. + return this.currentSite.invalidateWsCache().catch(() => { + // Ignore errors. + }).then(() => { + return this.loginHelper.goToSiteInitialPage(this.navCtrl, true); + }); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error.message, 'Error accepting site policy.'); + }).finally(() => { + modal.dismiss(); + }); + } +} diff --git a/src/core/login/providers/helper.ts b/src/core/login/providers/helper.ts index 5d3faa201..1b2decb2c 100644 --- a/src/core/login/providers/helper.ts +++ b/src/core/login/providers/helper.ts @@ -65,7 +65,17 @@ export class CoreLoginHelperProvider { if (!result.status) { // Error. if (result.warnings && result.warnings.length) { - return Promise.reject(result.warnings[0].message); + // Check if there is a warning 'alreadyagreed'. + for (let i in result.warnings) { + let warning = result.warnings[i]; + if (warning.warningcode == 'alreadyagreed') { + // Policy already agreed, treat it as a success. + return; + } + } + + // Another warning, reject. + return Promise.reject(result.warnings[0]); } else { return Promise.reject(null); } diff --git a/src/providers/app.ts b/src/providers/app.ts index 5b49e96b1..921781a5a 100644 --- a/src/providers/app.ts +++ b/src/providers/app.ts @@ -46,9 +46,10 @@ export class CoreAppProvider { ssoAuthenticationPromise : Promise; isKeyboardShown: boolean = false; - constructor(private dbProvider: CoreDbProvider, private platform: Platform, private keyboard: Keyboard, + constructor(dbProvider: CoreDbProvider, private platform: Platform, private keyboard: Keyboard, private network: Network, logger: CoreLoggerProvider) { this.logger = logger.getInstance('CoreAppProvider'); + this.db = dbProvider.getDB(this.DBNAME); this.keyboard.onKeyboardShow().subscribe((data) => { this.isKeyboardShown = true; @@ -92,10 +93,6 @@ export class CoreAppProvider { * @return {SQLiteDB} App's DB. */ getDB() : SQLiteDB { - if (typeof this.db == 'undefined') { - this.db = this.dbProvider.getDB(this.DBNAME); - } - return this.db; }; diff --git a/src/providers/logger.ts b/src/providers/logger.ts index e01300377..d196d24b6 100644 --- a/src/providers/logger.ts +++ b/src/providers/logger.ts @@ -62,7 +62,7 @@ export class CoreLoggerProvider { // Return our own function that will call the logging function with the treated message. return (...args) => { if (this.enabled) { - let now = moment().format('l LTS'); + let now = moment().format('l LTS'); args[0] = now + ' ' + className + ': ' + args[0]; // Prepend timestamp and className to the original message. logFn.apply(null, args);