forked from EVOgeek/Vmeda.Online
308 lines
13 KiB
TypeScript
308 lines
13 KiB
TypeScript
// (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()) +
|
|
'<p>' + this.translate.instant('core.networkerroriframemsg') + '</p>';
|
|
|
|
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 <object>. Try to get the window.
|
|
contentWindow = contentDocument.defaultView;
|
|
}
|
|
|
|
if (!contentWindow && element.getSVGDocument) {
|
|
// It's probably an <embed>. 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 (<a>) 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 (<a>) 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 = <Element> event.target;
|
|
while (el && el.tagName !== 'A') {
|
|
el = el.parentElement;
|
|
}
|
|
if (!el || el.tagName !== 'A') {
|
|
return;
|
|
}
|
|
const link = <HTMLAnchorElement> 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);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|