diff --git a/src/app/app.module.ts b/src/app/app.module.ts index b044b05f0..62d151460 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -36,6 +36,7 @@ import { CoreConfigProvider } from '@providers/config'; import { CoreLangProvider } from '@providers/lang'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreIframeUtilsProvider } from '@providers/utils/iframe'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreUrlUtilsProvider } from '@providers/utils/url'; import { CoreUtilsProvider } from '@providers/utils/utils'; @@ -127,6 +128,7 @@ export const CORE_PROVIDERS: any[] = [ CoreLangProvider, CoreTextUtilsProvider, CoreDomUtilsProvider, + CoreIframeUtilsProvider, CoreTimeUtilsProvider, CoreUrlUtilsProvider, CoreUtilsProvider, @@ -233,6 +235,7 @@ export const CORE_PROVIDERS: any[] = [ CoreLangProvider, CoreTextUtilsProvider, CoreDomUtilsProvider, + CoreIframeUtilsProvider, CoreTimeUtilsProvider, CoreUrlUtilsProvider, CoreUtilsProvider, diff --git a/src/components/iframe/iframe.ts b/src/components/iframe/iframe.ts index da98a380b..5988289a7 100644 --- a/src/components/iframe/iframe.ts +++ b/src/components/iframe/iframe.ts @@ -14,14 +14,9 @@ import { Component, Input, Output, OnInit, ViewChild, ElementRef, EventEmitter, OnChanges, SimpleChange } 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'; +import { CoreIframeUtilsProvider } from '@providers/utils/iframe'; /** */ @@ -40,12 +35,10 @@ export class CoreIframeComponent implements OnInit, OnChanges { 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) { + constructor(logger: CoreLoggerProvider, private iframeUtils: CoreIframeUtilsProvider, private domUtils: CoreDomUtilsProvider, + private sanitizer: DomSanitizer) { this.logger = logger.getInstance('CoreIframe'); this.loaded = new EventEmitter(); } @@ -62,7 +55,7 @@ export class CoreIframeComponent implements OnInit, OnChanges { // Show loading only with external URLs. this.loading = !this.src || !!this.src.match(/^https?:\/\//i); - this.treatFrame(iframe); + this.iframeUtils.treatFrame(iframe); if (this.loading) { iframe.addEventListener('load', () => { @@ -89,192 +82,4 @@ export class CoreIframeComponent implements OnInit, OnChanges { this.safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(changes.src.currentValue); } } - - /** - * 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); - - if (winAndDoc.window) { - // Send a resize events to the iframe so it calculates the right size if needed. - setTimeout(() => { - winAndDoc.window.dispatchEvent(new Event('resize')); - }, 1000); - } - }); - } - } - - /** - * 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.setAttribute('data', href); - } else { - e.preventDefault(); - element.setAttribute('src', href); - } - } - }); - } - } - }); - } - } diff --git a/src/directives/format-text.ts b/src/directives/format-text.ts index a0bbb17f6..d5a620daa 100644 --- a/src/directives/format-text.ts +++ b/src/directives/format-text.ts @@ -20,6 +20,7 @@ import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreIframeUtilsProvider } from '@providers/utils/iframe'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreUrlUtilsProvider } from '@providers/utils/url'; import { CoreUtilsProvider } from '@providers/utils/utils'; @@ -64,7 +65,8 @@ export class CoreFormatTextDirective implements OnChanges { private utils: CoreUtilsProvider, private urlUtils: CoreUrlUtilsProvider, private loggerProvider: CoreLoggerProvider, private filepoolProvider: CoreFilepoolProvider, private appProvider: CoreAppProvider, private contentLinksHelper: CoreContentLinksHelperProvider, @Optional() private navCtrl: NavController, - @Optional() private content: Content, @Optional() private svComponent: CoreSplitViewComponent) { + @Optional() private content: Content, @Optional() private svComponent: CoreSplitViewComponent, + private iframeUtils: CoreIframeUtilsProvider) { this.element = element.nativeElement; this.element.classList.add('opacity-hide'); // Hide contents until they're treated. this.afterRender = new EventEmitter(); @@ -315,7 +317,8 @@ export class CoreFormatTextDirective implements OnChanges { iframes, buttons, elementsWithInlineStyles, - stopClicksElements; + stopClicksElements, + frames; div.innerHTML = formatted; images = Array.from(div.querySelectorAll('img')); @@ -326,6 +329,7 @@ export class CoreFormatTextDirective implements OnChanges { buttons = Array.from(div.querySelectorAll('.button')); elementsWithInlineStyles = Array.from(div.querySelectorAll('*[style]')); stopClicksElements = Array.from(div.querySelectorAll('button,input,select,textarea')); + frames = Array.from(div.querySelectorAll(CoreIframeUtilsProvider.FRAME_TAGS.join(','))); // Walk through the content to find the links and add our directive to it. // Important: We need to look for links first because in 'img' we add new links without core-link. @@ -390,6 +394,11 @@ export class CoreFormatTextDirective implements OnChanges { }); }); + // Handle all kind of frames. + frames.forEach((frame: any) => { + this.iframeUtils.treatFrame(frame); + }); + return div; }); } diff --git a/src/providers/utils/iframe.ts b/src/providers/utils/iframe.ts new file mode 100644 index 000000000..1d2287266 --- /dev/null +++ b/src/providers/utils/iframe.ts @@ -0,0 +1,233 @@ +// (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 { Injectable } from '@angular/core'; +import { Platform } from 'ionic-angular'; +import { CoreFileProvider } from '../file'; +import { CoreLoggerProvider } from '../logger'; +import { CoreSitesProvider } from '../sites'; +import { CoreDomUtilsProvider } from './dom'; +import { CoreTextUtilsProvider } from './text'; +import { CoreUrlUtilsProvider } from './url'; +import { CoreUtilsProvider } from './utils'; + +/* + * "Utils" service with helper functions for iframes, embed and similar. + */ +@Injectable() +export class CoreIframeUtilsProvider { + static FRAME_TAGS = ['iframe', 'frame', 'object', 'embed']; + + protected logger; + + constructor(logger: CoreLoggerProvider, private fileProvider: CoreFileProvider, private sitesProvider: CoreSitesProvider, + private urlUtils: CoreUrlUtilsProvider, private textUtils: CoreTextUtilsProvider, private utils: CoreUtilsProvider, + private domUtils: CoreDomUtilsProvider, private platform: Platform) { + this.logger = logger.getInstance('CoreUtilsProvider'); + } + + /** + * Given an element, return the content window and document. + * Please notice that the element should be an iframe, embed or similar. + * + * @param {any} element Element to treat (iframe, embed, ...). + * @return {{ window: Window, document: Document }} Window and Document. + */ + 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. + try { + contentDocument = element.getSVGDocument(); + } catch (ex) { + // Ignore errors. + } + + 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 }; + } + + /** + * Redefine the open method in the contentWindow of an element and the sub frames. + * Please notice that the element should be an iframe, embed or similar. + * + * @param {any} element Element to treat (iframe, embed, ...). + * @param {Window} contentWindow The window of the element contents. + * @param {Document} contentDocument The document of the element contents. + */ + 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. + CoreIframeUtilsProvider.FRAME_TAGS.forEach((tag) => { + const elements = Array.from(contentDocument.querySelectorAll(tag)); + elements.forEach((subElement) => { + this.treatFrame(subElement); + }); + }); + } + } + + /** + * 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 (iframe, embed, ...). + */ + 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.treatFrameLinks(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.treatFrameLinks(element, winAndDoc.document); + + if (winAndDoc.window) { + // Send a resize events to the iframe so it calculates the right size if needed. + setTimeout(() => { + winAndDoc.window.dispatchEvent(new Event('resize')); + }, 1000); + } + }); + } + } + + /** + * Search links () in a frame and open them in browser or InAppBrowser if needed. + * Only links that haven't been treated by the frame's Javascript will be treated. + * + * @param {any} element Element to treat (iframe, embed, ...). + * @param {Document} contentDocument The document of the element contents. + */ + treatFrameLinks(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.setAttribute('data', href); + } else { + e.preventDefault(); + element.setAttribute('src', href); + } + } + }); + } + } + }); + } +}