// (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 { CoreAppProvider } from '@providers/app'; import { CoreFileProvider } from '@providers/file'; import { CoreUrlUtilsProvider } from '@providers/utils/url'; import { Observable, Observer } from 'rxjs'; import { InAppBrowserEvent } from '@ionic-native/in-app-browser'; /** * Emulates the Cordova InAppBrowserObject in desktop apps. * We aren't extending InAppBrowserObject because its constructor also opens a window, so we'd end up with 2 windows. */ export class InAppBrowserObjectMock { protected window; protected browserWindow; protected screen; protected isSSO: boolean; constructor(appProvider: CoreAppProvider, private fileProvider: CoreFileProvider, private urlUtils: CoreUrlUtilsProvider, private url: string, target?: string, options: string = 'location=yes') { if (!appProvider.isDesktop()) { // This plugin is only supported in desktop. return; } this.browserWindow = require('electron').remote.BrowserWindow; this.screen = require('electron').screen; this.isSSO = !!(url && url.match(/\/launch\.php\?service=.+&passport=/)); let width = 800, height = 600, display; const bwOptions: any = { webPreferences: {} }; try { // Create a separate session for inappbrowser so we can clear its data without affecting the app. bwOptions.webPreferences.session = require('electron').remote.session.fromPartition('inappbrowsersession'); } catch (ex) { // Ignore errors. } if (screen) { display = this.screen.getPrimaryDisplay(); if (display && display.workArea) { width = display.workArea.width || width; height = display.workArea.height || height; } } // Create the BrowserWindow options based on the received options. bwOptions.width = width; bwOptions.height = height; if (options.indexOf('hidden=yes') != -1) { bwOptions.show = false; } if (options.indexOf('location=no') != -1) { bwOptions.frame = false; } if (options.indexOf('fullscreen=yes') != -1) { bwOptions.fullscreen = true; } if (options.indexOf('clearsessioncache=yes') != -1 && bwOptions.webPreferences.session) { bwOptions.webPreferences.session.clearStorageData({storages: 'cookies'}); } this.window = new this.browserWindow(bwOptions); this.window.loadURL(url); if (this.isSSO) { // SSO in Linux. Simulate it's an iOS device so we can retrieve the launch URL. // This is needed because custom URL scheme is not supported in Linux. const userAgent = 'Mozilla/5.0 (iPad) AppleWebKit/603.3.8 (KHTML, like Gecko) Mobile/14G60'; this.window.webContents.setUserAgent(userAgent); } } /** * Close the window. */ close(): void { if (this.window.isDestroyed()) { // Window already closed, nothing to do. return; } try { this.window.close(); } catch (ex) { // Ignore errors. } } /** * Execute a JS script. * * @param {any} details Details of the script to run, specifying either a file or code key. * @return {Promise} Promise resolved when done. */ executeScript(details: any): Promise { return new Promise((resolve, reject): void => { if (details.code) { this.window.webContents.executeJavaScript(details.code, false, resolve); } else if (details.file) { this.fileProvider.readFile(details.file).then((code) => { this.window.webContents.executeJavaScript(code, false, resolve); }).catch(reject); } else { reject('executeScript requires exactly one of code or file to be specified'); } }); } /** * Recursive function to get the launch URL from the contents of a BrowserWindow. * * @param {number} [retry=0] Retry number. * @return {Promise} Promise resolved with the launch URL. */ protected getLaunchUrl(retry: number = 0): Promise { if (this.window.isDestroyed()) { // Window is destroyed, stop. return Promise.reject(null); } return new Promise((resolve, reject): void => { // Execute Javascript to retrieve the launch link. const jsCode = 'var el = document.querySelector("#launchapp"); el && el.href;'; let found = false; this.window.webContents.executeJavaScript(jsCode).then((launchUrl) => { found = true; resolve(launchUrl); }); setTimeout(() => { if (found) { // URL found, stop. } else if (retry > 5) { // Waited enough, stop. reject(); } else { this.getLaunchUrl(retry + 1).then(resolve, reject); } }, 300); }); } /** * Hide the window. */ hide(): void { this.window.hide(); } /** * Insert CSS. * * @param {any} details Details of the CSS to insert, specifying either a file or code key. * @return {Promise} Promise resolved when done. */ insertCSS(details: any): Promise { return new Promise((resolve, reject): void => { if (details.code) { this.window.webContents.insertCSS(details.code); resolve(); } else if (details.file) { this.fileProvider.readFile(details.file).then((code) => { this.window.webContents.insertCSS(code); resolve(); }).catch(reject); } else { reject('insertCSS requires exactly one of code or file to be specified'); } }); } /** * Listen to events happening. * * @param {string} name Name of the event. * @return {Observable} Observable that will listen to the event on subscribe, and will stop listening * to the event on unsubscribe. */ on(name: string): Observable { // Create the observable. return new Observable((observer: Observer): any => { // Helper functions to handle events. const received = (event, url): void => { try { event.url = url || (this.window.isDestroyed() ? '' : this.window.getURL()); event.type = name; observer.next(event); } catch (ex) { // Ignore errors. } }, finishLoad = (event): void => { // Check if user is back to launch page. if (this.urlUtils.removeUrlParams(this.url) == this.urlUtils.removeUrlParams(this.window.getURL())) { // The launch page was loaded. Search for the launch link. this.getLaunchUrl().then((launchUrl) => { if (launchUrl) { // Launch URL retrieved, send it and stop listening. received(event, launchUrl); } }); } }; if (!this.window.isDestroyed() && !this.window.webContents.isDestroyed()) { switch (name) { case 'loadstart': this.window.webContents.on('did-start-loading', received); if (this.isSSO) { // Linux doesn't support custom URL Schemes. Check if launch page is loaded. this.window.webContents.on('did-finish-load', finishLoad); } break; case 'loadstop': this.window.webContents.on('did-finish-load', received); break; case 'loaderror': this.window.webContents.on('did-fail-load', received); break; case 'exit': this.window.on('close', received); break; default: } } return (): void => { // Unsubscribing. We need to remove the listeners. if (this.window.isDestroyed() || this.window.webContents.isDestroyed()) { // Page has been destroyed already, no need to remove listeners. return; } switch (name) { case 'loadstart': this.window.webContents.removeListener('did-start-loading', received); this.window.webContents.removeListener('did-finish-load', finishLoad); break; case 'loadstop': this.window.webContents.removeListener('did-finish-load', received); break; case 'loaderror': this.window.webContents.removeListener('did-fail-load', received); break; case 'exit': this.window.removeListener('close', received); break; default: } }; }); } /** * Show the window. */ show(): void { this.window.show(); } }