// (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, NgZone } from '@angular/core'; import { Platform } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { Network } from '@ionic-native/network'; import { CoreAppProvider } from '../app'; 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, private appProvider: CoreAppProvider, private translate: TranslateService, private network: Network, private zone: NgZone) { this.logger = logger.getInstance('CoreUtilsProvider'); } /** * Check if a frame uses an online URL but the app is offline. If it does, the iframe is hidden and a warning is shown. * * @param {any} element The frame to check (iframe, embed, ...). * @param {boolean} [isSubframe] Whether it's a frame inside another frame. * @return {boolean} True if frame is online and the app is offline, false otherwise. */ checkOnlineFrameInOffline(element: any, isSubframe?: boolean): boolean { const src = element.src || element.data; if (src && src.match(/^https?:\/\//i) && !this.appProvider.isOnline()) { if (element.classList.contains('core-iframe-offline-disabled')) { // Iframe already hidden, stop. return true; } // The frame has an online URL but the app is offline. Show a warning. const div = document.createElement('div'); div.setAttribute('text-center', ''); div.setAttribute('padding', ''); div.classList.add('core-iframe-offline-warning'); div.innerHTML = (isSubframe ? '' : this.domUtils.getConnectionWarningIconHtml()) + '

' + this.translate.instant('core.networkerroriframemsg') + '

'; element.parentElement.insertBefore(div, element); // Add a class to specify that the iframe is hidden. element.classList.add('core-iframe-offline-disabled'); if (isSubframe) { // We cannot apply CSS styles in subframes, just hide the iframe. element.style.display = 'none'; } // If the network changes, check it again. const subscription = this.network.onConnect().subscribe(() => { // Execute the callback in the Angular zone, so change detection doesn't stop working. this.zone.run(() => { if (!this.checkOnlineFrameInOffline(element, isSubframe)) { // Now the app is online, no need to check connection again. subscription.unsubscribe(); } }); }); return true; } else if (element.classList.contains('core-iframe-offline-disabled')) { // Reload the frame. element.src = element.src; element.data = element.data; // Remove the warning and show the iframe this.domUtils.removeElement(element.parentElement, 'div.core-iframe-offline-warning'); element.classList.remove('core-iframe-offline-disabled'); if (isSubframe) { element.style.display = ''; } } return false; } /** * 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; try { contentDocument = element.contentDocument || (contentWindow && contentWindow.document); } catch (ex) { // Ignore errors. } 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 null; } } else { this.logger.warn('Cannot get iframe src to open relative url', url, element); return null; } } 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); } } // We cannot create new Window objects directly, return null which is a valid return value for Window.open(). return null; }; } if (contentDocument) { // Search sub frames. CoreIframeUtilsProvider.FRAME_TAGS.forEach((tag) => { const elements = Array.from(contentDocument.querySelectorAll(tag)); elements.forEach((subElement) => { this.treatFrame(subElement, true); }); }); } } /** * 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, ...). * @param {boolean} [isSubframe] Whether it's a frame inside another frame. */ treatFrame(element: any, isSubframe?: boolean): void { if (element) { this.checkOnlineFrameInOffline(element, isSubframe); 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', () => { this.checkOnlineFrameInOffline(element, isSubframe); // 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; } contentDocument.addEventListener('click', (event) => { if (event.defaultPrevented) { // Event already prevented by some other code. return; } // Find the link being clicked. let el = event.target; while (el && el.tagName !== 'A') { el = el.parentElement; } if (!el || el.tagName !== 'A') { return; } const link = el; const scheme = this.urlUtils.getUrlScheme(link.href); if (!link.href || (scheme && scheme == 'javascript')) { // Links with no URL and Javascript links are ignored. return; } if (scheme && scheme != 'file' && scheme != 'filesystem') { // Scheme suggests it's an external resource, open it in browser. event.preventDefault(); if (!this.sitesProvider.isLoggedIn()) { this.utils.openInBrowser(link.href); } else { this.sitesProvider.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(link.href); } } else if (link.target == '_parent' || link.target == '_top' || link.target == '_blank') { // Opening links with _parent, _top or _blank can break the app. We'll open it in InAppBrowser. event.preventDefault(); this.utils.openFile(link.href).catch((error) => { this.domUtils.showErrorModal(error); }); } else if (this.platform.is('ios') && (!link.target || link.target == '_self')) { // In cordova ios 4.1.0 links inside iframes stopped working. We'll manually treat them. event.preventDefault(); if (element.tagName.toLowerCase() == 'object') { element.setAttribute('data', link.href); } else { element.setAttribute('src', link.href); } } }); } }