├── .gitignore ├── LICENSE.txt ├── assets ├── defaultImages │ ├── android │ │ ├── hdpi.png │ │ ├── land-hdpi.png │ │ ├── land-ldpi.png │ │ ├── land-mdpi.png │ │ ├── land-xhdpi.png │ │ ├── ldpi.png │ │ ├── mdpi.png │ │ ├── port-hdpi.png │ │ ├── port-ldpi.png │ │ ├── port-mdpi.png │ │ ├── port-xhdpi.png │ │ ├── xhdpi.png │ │ ├── xxhdpi.png │ │ └── xxxhdpi.png │ ├── ios │ │ ├── 100x100.png │ │ ├── 1024x768.png │ │ ├── 114x114.png │ │ ├── 120x120.png │ │ ├── 1242x2208.png │ │ ├── 144x144.png │ │ ├── 152x152.png │ │ ├── 1536x2048.png │ │ ├── 180x180.png │ │ ├── 2048x1536.png │ │ ├── 2208x1242.png │ │ ├── 29x29.png │ │ ├── 320x480.png │ │ ├── 40x40.png │ │ ├── 50x50.png │ │ ├── 57x57.png │ │ ├── 58x58.png │ │ ├── 60x60.png │ │ ├── 640x1136.png │ │ ├── 640x960.png │ │ ├── 72x72.png │ │ ├── 750x1334.png │ │ ├── 768x1024.png │ │ ├── 76x76.png │ │ └── 80x80.png │ ├── windows │ │ ├── 106x106.png │ │ ├── 1152x1920.png │ │ ├── 120x120.png │ │ ├── 150x150.png │ │ ├── 170x170.png │ │ ├── 30x30.png │ │ ├── 310x150.png │ │ ├── 310x310.png │ │ ├── 360x360.png │ │ ├── 44x44.png │ │ ├── 50x50.png │ │ ├── 620x300.png │ │ ├── 70x70.png │ │ ├── 71x71.png │ │ └── 744x360.png │ └── wp8 │ │ ├── 173x173.png │ │ ├── 480x800.png │ │ └── 62x62.png └── windows │ ├── wrapper.css │ ├── wrapper.html │ └── wrapper.js ├── index.js ├── package.json ├── plugin.xml ├── readme.md ├── scripts ├── createConfigParser.js ├── downloader.js ├── package.json ├── replaceWindowsWrapperFiles.js ├── rollbackWindowsWrapperFiles.js ├── test │ ├── assets │ │ ├── fullAccessRules │ │ │ ├── config.xml │ │ │ ├── manifest.json │ │ │ └── www │ │ │ │ └── empty.txt │ │ ├── fullUrlForScope │ │ │ ├── config.xml │ │ │ ├── manifest.json │ │ │ └── www │ │ │ │ └── empty.txt │ │ ├── jsonPropertiesMissing │ │ │ ├── config.xml │ │ │ ├── manifest.json │ │ │ └── www │ │ │ │ └── empty.txt │ │ ├── logo.png │ │ ├── normalFlow │ │ │ ├── config.xml │ │ │ ├── manifest.json │ │ │ └── www │ │ │ │ └── empty.txt │ │ ├── shortNameMissing │ │ │ ├── config.xml │ │ │ ├── manifest.json │ │ │ └── www │ │ │ │ └── empty.txt │ │ ├── shortNameWithSlashes │ │ │ ├── config.xml │ │ │ ├── manifest.json │ │ │ └── www │ │ │ │ └── empty.txt │ │ ├── wildcardSubdomainForScope │ │ │ ├── config.xml │ │ │ ├── manifest.json │ │ │ └── www │ │ │ │ └── empty.txt │ │ └── xmlEmptyWidget │ │ │ ├── config.xml │ │ │ ├── manifest.json │ │ │ └── www │ │ │ └── empty.txt │ ├── downloader.js │ ├── readme.md │ ├── test-utils.js │ └── updateConfigurationBeforePrepare.js ├── updateConfigurationAfterPrepare.js └── updateConfigurationBeforePrepare.js ├── src ├── android │ └── HostedWebApp.java ├── ios │ ├── CDVHostedWebApp.h │ └── CDVHostedWebApp.m └── windows │ └── HostedWebAppPluginProxy.js └── www ├── hostedWebApp.js └── hostedapp-bridge.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /scripts/node_modules/ 3 | npm-debug.log 4 | coverage.html 5 | .ntvs_analysis.dat 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | ManifoldJS 2 | 3 | Copyright (c) Microsoft Corporation 4 | 5 | All rights reserved. 6 | 7 | MIT License 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ""Software""), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | -------------------------------------------------------------------------------- /assets/defaultImages/android/hdpi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/android/hdpi.png -------------------------------------------------------------------------------- /assets/defaultImages/android/land-hdpi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/android/land-hdpi.png -------------------------------------------------------------------------------- /assets/defaultImages/android/land-ldpi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/android/land-ldpi.png -------------------------------------------------------------------------------- /assets/defaultImages/android/land-mdpi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/android/land-mdpi.png -------------------------------------------------------------------------------- /assets/defaultImages/android/land-xhdpi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/android/land-xhdpi.png -------------------------------------------------------------------------------- /assets/defaultImages/android/ldpi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/android/ldpi.png -------------------------------------------------------------------------------- /assets/defaultImages/android/mdpi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/android/mdpi.png -------------------------------------------------------------------------------- /assets/defaultImages/android/port-hdpi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/android/port-hdpi.png -------------------------------------------------------------------------------- /assets/defaultImages/android/port-ldpi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/android/port-ldpi.png -------------------------------------------------------------------------------- /assets/defaultImages/android/port-mdpi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/android/port-mdpi.png -------------------------------------------------------------------------------- /assets/defaultImages/android/port-xhdpi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/android/port-xhdpi.png -------------------------------------------------------------------------------- /assets/defaultImages/android/xhdpi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/android/xhdpi.png -------------------------------------------------------------------------------- /assets/defaultImages/android/xxhdpi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/android/xxhdpi.png -------------------------------------------------------------------------------- /assets/defaultImages/android/xxxhdpi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/android/xxxhdpi.png -------------------------------------------------------------------------------- /assets/defaultImages/ios/100x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/ios/100x100.png -------------------------------------------------------------------------------- /assets/defaultImages/ios/1024x768.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/ios/1024x768.png -------------------------------------------------------------------------------- /assets/defaultImages/ios/114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/ios/114x114.png -------------------------------------------------------------------------------- /assets/defaultImages/ios/120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/ios/120x120.png -------------------------------------------------------------------------------- /assets/defaultImages/ios/1242x2208.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/ios/1242x2208.png -------------------------------------------------------------------------------- /assets/defaultImages/ios/144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/ios/144x144.png -------------------------------------------------------------------------------- /assets/defaultImages/ios/152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/ios/152x152.png -------------------------------------------------------------------------------- /assets/defaultImages/ios/1536x2048.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/ios/1536x2048.png -------------------------------------------------------------------------------- /assets/defaultImages/ios/180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/ios/180x180.png -------------------------------------------------------------------------------- /assets/defaultImages/ios/2048x1536.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/ios/2048x1536.png -------------------------------------------------------------------------------- /assets/defaultImages/ios/2208x1242.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/ios/2208x1242.png -------------------------------------------------------------------------------- /assets/defaultImages/ios/29x29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/ios/29x29.png -------------------------------------------------------------------------------- /assets/defaultImages/ios/320x480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/ios/320x480.png -------------------------------------------------------------------------------- /assets/defaultImages/ios/40x40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/ios/40x40.png -------------------------------------------------------------------------------- /assets/defaultImages/ios/50x50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/ios/50x50.png -------------------------------------------------------------------------------- /assets/defaultImages/ios/57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/ios/57x57.png -------------------------------------------------------------------------------- /assets/defaultImages/ios/58x58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/ios/58x58.png -------------------------------------------------------------------------------- /assets/defaultImages/ios/60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/ios/60x60.png -------------------------------------------------------------------------------- /assets/defaultImages/ios/640x1136.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/ios/640x1136.png -------------------------------------------------------------------------------- /assets/defaultImages/ios/640x960.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/ios/640x960.png -------------------------------------------------------------------------------- /assets/defaultImages/ios/72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/ios/72x72.png -------------------------------------------------------------------------------- /assets/defaultImages/ios/750x1334.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/ios/750x1334.png -------------------------------------------------------------------------------- /assets/defaultImages/ios/768x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/ios/768x1024.png -------------------------------------------------------------------------------- /assets/defaultImages/ios/76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/ios/76x76.png -------------------------------------------------------------------------------- /assets/defaultImages/ios/80x80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/ios/80x80.png -------------------------------------------------------------------------------- /assets/defaultImages/windows/106x106.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/windows/106x106.png -------------------------------------------------------------------------------- /assets/defaultImages/windows/1152x1920.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/windows/1152x1920.png -------------------------------------------------------------------------------- /assets/defaultImages/windows/120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/windows/120x120.png -------------------------------------------------------------------------------- /assets/defaultImages/windows/150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/windows/150x150.png -------------------------------------------------------------------------------- /assets/defaultImages/windows/170x170.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/windows/170x170.png -------------------------------------------------------------------------------- /assets/defaultImages/windows/30x30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/windows/30x30.png -------------------------------------------------------------------------------- /assets/defaultImages/windows/310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/windows/310x150.png -------------------------------------------------------------------------------- /assets/defaultImages/windows/310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/windows/310x310.png -------------------------------------------------------------------------------- /assets/defaultImages/windows/360x360.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/windows/360x360.png -------------------------------------------------------------------------------- /assets/defaultImages/windows/44x44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/windows/44x44.png -------------------------------------------------------------------------------- /assets/defaultImages/windows/50x50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/windows/50x50.png -------------------------------------------------------------------------------- /assets/defaultImages/windows/620x300.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/windows/620x300.png -------------------------------------------------------------------------------- /assets/defaultImages/windows/70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/windows/70x70.png -------------------------------------------------------------------------------- /assets/defaultImages/windows/71x71.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/windows/71x71.png -------------------------------------------------------------------------------- /assets/defaultImages/windows/744x360.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/windows/744x360.png -------------------------------------------------------------------------------- /assets/defaultImages/wp8/173x173.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/wp8/173x173.png -------------------------------------------------------------------------------- /assets/defaultImages/wp8/480x800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/wp8/480x800.png -------------------------------------------------------------------------------- /assets/defaultImages/wp8/62x62.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/assets/defaultImages/wp8/62x62.png -------------------------------------------------------------------------------- /assets/windows/wrapper.css: -------------------------------------------------------------------------------- 1 | body { width: 100%; height: 100%; position: absolute; top: 0; left: 0; margin: 0; } .loading-progress { position: absolute; top: 50%; left: 45%; width: 10%; } .extendedSplashScreen .loading-progress { position: absolute; top: 50%; left: 20%; width: 60%; margin: 0; } .extendedSplashScreen { display: block; background-color: #000000; height: 100%; width: 100%; position: absolute; top: 0px; left: 0px; text-align: center; z-index: 10111; } .extendedSplashScreen .extendedSplashImage { position: absolute; } -------------------------------------------------------------------------------- /assets/windows/wrapper.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | Hello World 9 | 10 | 11 |
12 | Launching... 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /assets/windows/wrapper.js: -------------------------------------------------------------------------------- 1 | var setupExtendedSplashScreen, updateSplashScreenPositioning, 2 | splashScreen, splashScreenEl, splashScreenImageEl, 3 | isWindows = navigator.appVersion.indexOf("Windows Phone") === -1, 4 | isWindowsPhone10 = navigator.appVersion.indexOf("Windows Phone 10") !== -1; 5 | 6 | // TODO: Need to fix styling issues whith the extended splash screen for Windows Phone 10 (disabled for now) 7 | if (!isWindowsPhone10) { 8 | WinJS.Application.addEventListener("activated", function (e) { 9 | if (e.detail.kind === Windows.ApplicationModel.Activation.ActivationKind.launch) { 10 | splashScreen = e.detail.splashScreen; 11 | 12 | // Listen for window resize events to reposition the extended splash screen image accordingly. 13 | // This is important to ensure that the extended splash screen is formatted properly in response to snapping, unsnapping, rotation, etc... 14 | window.addEventListener("resize", updateSplashPositioning, false); 15 | 16 | var previousExecutionState = e.detail.previousExecutionState; 17 | var state = Windows.ApplicationModel.Activation.ApplicationExecutionState; 18 | if (previousExecutionState === state.notRunning 19 | || previousExecutionState === state.terminated 20 | || previousExecutionState === state.closedByUser) { 21 | setupExtendedSplashScreen(); 22 | } 23 | } 24 | }, false); 25 | } 26 | 27 | setupExtendedSplashScreen = function () { 28 | splashScreenEl = document.getElementById("extendedSplashScreen"); 29 | splashScreenImageEl = (splashScreenEl && splashScreenEl.querySelector(".extendedSplashImage")); 30 | splashLoadingEl = (splashScreenEl && splashScreenEl.querySelector(".loading-progress")); 31 | 32 | if (!splashScreen || !splashScreenEl || !splashScreenImageEl) { return; } 33 | 34 | var imgSrc = "/images/splashScreenPhone.png" 35 | if (isWindows) { 36 | imgSrc = "/images/SplashScreen.png" 37 | } 38 | 39 | splashScreenImageEl.setAttribute("src", imgSrc); 40 | 41 | updateSplashPositioning(); 42 | 43 | // Once the extended splash screen is setup, apply the CSS style that will make the extended splash screen visible. 44 | splashScreenEl.style.display = "block"; 45 | }; 46 | 47 | updateSplashPositioning = function () { 48 | if (!splashScreen || !splashScreenImageEl) { return; } 49 | // Position the extended splash screen image in the same location as the system splash screen image. 50 | if (isWindows) { 51 | splashScreenImageEl.style.top = splashScreen.imageLocation.y + "px"; 52 | splashScreenImageEl.style.left = splashScreen.imageLocation.x + "px"; 53 | splashScreenImageEl.style.height = splashScreen.imageLocation.height + "px"; 54 | splashScreenImageEl.style.width = splashScreen.imageLocation.width + "px"; 55 | } else { 56 | var curOrientation = Windows.Devices.Sensors.SimpleOrientationSensor.getDefault().getCurrentOrientation(); 57 | if ((curOrientation == Windows.Devices.Sensors.SimpleOrientation.rotated270DegreesCounterclockwise || curOrientation == Windows.Devices.Sensors.SimpleOrientation.rotated90DegreesCounterclockwise) && 58 | Windows.Graphics.Display.DisplayInformation.autoRotationPreferences != Windows.Graphics.Display.DisplayOrientations.portrait) { 59 | splashScreenImageEl.src = "/images/splashscreen.png"; 60 | } else { 61 | splashScreenImageEl.src = "/images/splashScreenPhone.png"; 62 | } 63 | splashScreenImageEl.style.width = "100%"; 64 | splashScreenImageEl.style.height = "100%"; 65 | } 66 | 67 | if (splashLoadingEl) { 68 | if (isWindows) { 69 | splashLoadingEl.style.top = (splashScreen.imageLocation.y + splashScreen.imageLocation.height + 20) + "px"; 70 | } else { 71 | splashLoadingEl.style.top = (window.innerHeight * 0.8) + "px"; 72 | } 73 | } 74 | }; 75 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Usage 4 | * ----- 5 | * 6 | * var ManifoldCordovaPluginPath = require('ManifoldCordova'); 7 | * 8 | */ 9 | 10 | module.exports = __dirname; 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cordova-plugin-hostedwebapp", 3 | "version": "0.3.1", 4 | "description": "Hosted Web App Plugin", 5 | "cordova": { 6 | "id": "cordova-plugin-hostedwebapp", 7 | "platforms": [ 8 | "android", 9 | "windows", 10 | "ios" 11 | ] 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/manifoldjs/ManifoldCordova.git" 16 | }, 17 | "keywords": [ 18 | "cordova", 19 | "manifest", 20 | "hosted", 21 | "web", 22 | "ecosystem:cordova", 23 | "cordova-android", 24 | "cordova-windows", 25 | "cordova-ios" 26 | ], 27 | "author": "", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/manifoldjs/ManifoldCordova/issues" 31 | }, 32 | "homepage": "https://github.com/manifoldjs/ManifoldCordova" 33 | } 34 | -------------------------------------------------------------------------------- /plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | HostedWebApp 6 | Hosted Web App Plugin 7 | MIT License 8 | cordova,manifest,hosted,web,manifoldjs 9 | https://github.com/manifoldjs/ManifoldCordova.git 10 | https://github.com/manifoldjs/ManifoldCordova/issues 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |  4 | 5 | # Hosted Web Application 6 | This plugin enables the creation of a hosted web application from a [W3C manifest](http://www.w3.org/2008/webapps/manifest/) that provides metadata associated with a web site. It uses properties in the manifest to update corresponding properties in the Cordova configuration file to enable using content hosted in the site inside a Cordova application. 7 | 8 | **Typical manifest** 9 |
 10 | {
 11 |   "lang": "en",
 12 |   "name": "Super Racer 2000",
 13 |   "short_name": "Racer2K",
 14 |   "icons": [{
 15 |         "src": "icon/lowres",
 16 |         "sizes": "64x64",
 17 |         "type": "image/webp"
 18 |       }, {
 19 |         "src": "icon/hd_small",
 20 |         "sizes": "64x64"
 21 |       }, {
 22 |         "src": "icon/hd_hi",
 23 |         "sizes": "128x128",
 24 |         "density": 2
 25 |       }],
 26 |   "scope": "/racer/",
 27 |   "start_url": "http://www.racer2k.net/racer/start.html",
 28 |   "display": "fullscreen",
 29 |   "orientation": "landscape",
 30 |   "theme_color": "aliceblue"
 31 | }
 32 | 
33 | 34 | The W3C manifest enables the configuration of the application’s name, its starting URL, default orientation, and the icons it uses. In addition, it will update the application’s security policy to control access to external domains. 35 | 36 | When the application is launched, the plugin automatically handles navigation to the site’s starting URL. 37 | 38 | > **Note:** Although the W3C specs for the Web App manifest consider absolute and relative URLs valid for the _start_url_ value (e.g. _http://www.racer2k.net/racer/start.html_ and _/start.html_ are both valid), the plugin requires this URL **to be an absolute URL**. Otherwise, the installed applications won't be able to navigate to the web site. 39 | 40 | The plugin enables the injection of additional Cordova plugins and app-specific scripts that consume them allowing you to take advantage of native features in your hosted web apps. 41 | 42 | Lastly, since network connectivity is essential to the operation of a hosted web application, the plugin implements a basic offline feature that will show an offline page whenever connectivity is lost and will prevent users from interacting with the application until the connection is restored. 43 | 44 | ## Installation 45 | `cordova plugin add cordova-plugin-hostedwebapp` 46 | 47 | > **IMPORTANT:** Before using the plugin, make sure to copy the W3C manifest file to the **root** folder of the Cordova application, alongside **config.xml**, and name it **manifest.json**. 48 | 49 | ## Design 50 | The plugin behavior is mostly implemented at build time by mapping properties in the W3C manifest to standard Cordova settings defined in the **config.xml** file. 51 | 52 | This mapping process is handled by a hook that executes during the **before_prepare** stage of the Cordova build process. The hook updates the **config.xml** file with values obtained from the manifest. 53 | 54 | The plugin hook also handles downloading any icons that are specified in the manifest and copies them to the application’s directory, using their dimensions, and possibly their pixel density, to classify them as either an icon or a splash screen, as well as determining the platform for which they are suitable (e.g. iOS, Android, Windows, etc.). It uses this information to configure the corresponding icon and splash elements for each supported platform. 55 | 56 | ## Getting Started 57 | 58 | The following tutorial requires you to install the [Cordova Command-Line Inteface](http://cordova.apache.org/docs/en/4.0.0/guide_cli_index.md.html#The%20Command-Line%20Interface). 59 | 60 | ### Hosting a Web Application 61 | The plugin enables using content hosted in a web site inside a Cordova application by providing a manifest that describes the site. 62 | 63 | 1. Create a new Cordova application. 64 | `cordova create sampleapp yourdomain.sampleapp SampleHostedApp` 65 | 66 | 1. Go to the **sampleapp** directory created by the previous command. 67 | 68 | 1. Download or create a [W3C manifest](http://www.w3.org/2008/webapps/manifest/) describing the website to be hosted by the Cordova application and copy this file to its **root** folder, alongside **config.xml**. If necessary, rename the file as **manifest.json**. 69 | 70 | > **Note:** You can find a sample manifest file at the start of this document. 71 | 72 | 1. Add the **Hosted Web Application** plugin to the project. 73 | `cordova plugin add cordova-plugin-hostedwebapp` 74 | 75 | 1. Add one or more platforms, for example, to support Android. 76 | `cordova platform add android` 77 | 78 | 1. Build the application. 79 | `cordova build` 80 | 81 | 1. Launch the application in the emulator for one of the supported platforms. For example: 82 | `cordova emulate android` 83 | 84 | > **Note:** The plugin updates the Cordova configuration file (config.xml) with the information in the W3C manifest. If the information in the manifest changes, you can reapply the updated manifest settings at any time by executing prepare. For example: 85 | `cordova prepare` 86 | 87 | ### Using Cordova Plugins in Hosted Web Apps 88 | The plugin supports the injection of Cordova and the plugin interface scripts into the pages of a hosted site. There are two different plugin modes: '_server_' and '_client_'. In '_client_' mode, the **cordova.js** file and the plugin interface script files are retrieved from the app package. In '_server_' mode, these files are downloaded from the server along with the rest of the app's content. The plugin also provides a mechanism for injecting scripts that can be used, among other things, to consume the plugins added to the app. Imported scripts can be retrieved from the app package or downloaded from a remote source. 89 | 90 | Very briefly, these are the steps that are needed to use plugins: 91 | 92 | - Add one or more Cordova plugins to the app. 93 | 94 | - Enable API access in any pages where Cordova and the plugins will be used. This injects the Cordova runtime environment and is configured via a custom extension in the W3C manifest. The **match** and **platform** attributes specifies the pages and platforms where you will use Cordova. 95 | 96 | ``` 97 | { 98 | ... 99 | "mjs_api_access": [ 100 | { "match": "http://yoursite.com/path1/*", "platform": "android, ios, windows", "access": "cordova" }, 101 | ... 102 | ] 103 | } 104 | ``` 105 | - Optionally, choose a plugin mode. The default mode is _client_. 106 | 107 | **Client mode** 108 | ``` 109 | { 110 | ... 111 | "mjs_cordova": { 112 | "plugin_mode": "client" 113 | } 114 | } 115 | ``` 116 | 117 | **Server mode** 118 | ``` 119 | { 120 | ... 121 | "mjs_cordova": { 122 | "plugin_mode": "server", 123 | "base_url": "js/cordova" 124 | } 125 | } 126 | ``` 127 | 128 | (In '_server_' mode, the Cordova files and plugin interface scripts must be deployed to the site to the path specified in **base_url**. Also, the **cordova.js** and **cordova_plugins.js** files for each platform need to be renamed to specify the platform in their names so that **cordova.js** and **cordova_plugins.js** become, in the case of Android for example, **cordova-android.js** and **cordova_plugins-android.js** respectively.) 129 | 130 | To inject scripts into the hosted web content: 131 | 132 | - Update the app's manifest to list the imported scripts in a custom **mjs_import_scripts** section. 133 | ``` 134 | { 135 | ... 136 | "mjs_import_scripts": [ 137 | { "src": "js/alerts.js" }, 138 | { "src": "http://yoursite.com/js/app/contacts.js" }, 139 | { "src": "js/camera.js", "match": "http://yoursite.com/profile/*" }, 140 | ... 141 | ] 142 | } 143 | ``` 144 | 145 | - For app-hosted scripts, copy the script files to the Cordova project. The path in **mjs_import_scripts** must be specified relative to the '_www_' folder of the project. Server-hosted scripts must be deployed to the site. 146 | 147 | The following [wiki article](https://github.com/manifoldjs/ManifoldJS/wiki/Using-Cordova-Plugins-in-Hosted-Web-Apps) provides additional information about these features. 148 | 149 | ### Offline Feature 150 | The plugin implements an offline feature that will show an offline page whenever network connectivity is lost. 151 | 152 | The feature is enabled by default, but can be disabled with the following property in the manifest.json file. 153 | 154 | ``` 155 | { 156 | ... 157 | "mjs_offline_feature": false 158 | ... 159 | } 160 | ``` 161 | 162 | By default, the page shows a suitable message informing the user about the loss of connectivity. To customize the offline experience, a page named **offline.html** can be placed in the **www** folder of the application and it will be used instead. 163 | 164 | 1. To test the offline feature, interrupt the network connection to show the offline page and reconnect it to hide it. 165 | 166 | > **Note:** The procedure for setting offline mode varies depending on whether you are testing on an actual device or an emulator. In devices, you can simply set the device to airplane mode. In the case of simulators there is no single method. For example, in [Ripple](http://ripple.incubator.apache.org/), you can simulate a network disconnection by setting the Connection Type to 'none' under Network Status. On the other hand, for the iOS Simulator, you may need to physically disconnect the network cable or turn off the WiFi connection of the host machine. 167 | 168 | 1. Optionally, replace the default offline UI by adding a new page with the content to be shown while in offline mode. Name the page **offline.html** and place it in the **www** folder of the project. 169 | 170 | ### Icons and Splash Screens 171 | The plugin uses any icons specified in the W3C manifest to configure the Cordova application. However, specifying icons in the manifest is not mandatory. If the W3C manifest does not specify any, the application will continue to use the default Cordova icon set or you can enter icon and splash elements manually in the **config.xml** file and they will be used instead. However, be aware that the plugin does replace any such elements if it finds an icon in the manifest that matches its size. Typically, manifest entries reference icons hosted by the target site itself and should reference suitable icons for each platform supported by the application, as described in the [W3C spec](http://www.w3.org/2008/webapps/manifest/#icon-object-and-its-members). The plugin takes care of downloading the corresponding files and copies them to the correct locations in the project. 172 | 173 | When you run **cordova prepare**, the plugin will download from the hosted site all image assets in the manifest, if they are available, and it will store them inside the Cordova project using their relative paths as specified in the manifest. You can add any icons missing from the site or replace any icons that were downloaded by simply copying them to the correct location inside the project always making sure that they match the relative path in the manifest. Once the images are in place, building the project will copy the icons to each platform specific folder at the correct locations. 174 | 175 | For example, the following manifest references icons from the _/resources_ path of the site, for example, _/resources/android/icons/icon-36-ldpi.png_. The plugin expects the corresponding icon file to be stored in the same path relative to the root of the Cordova project. 176 | 177 |
178 | {
179 |     "name": "Super Racer 2000",
180 |     "short_name": "Racer2K",
181 |     "icons": [
182 |         {
183 |             "src": "/resources/android/icons/icon-36-ldpi.png",
184 |             "sizes": "36x36"
185 |         },
186 |         {
187 |             "src": "/resources/android/icons/icon-48-mdpi.png",
188 |             "sizes": "48x48"
189 |         },
190 |         ...
191 |         {
192 |             "src": "/resources/ios/icons/icon-40-2x.png",
193 |             "sizes": "80x80"
194 |         },
195 |         ...
196 |         {
197 |             "src": "/resources/windows/icons/Square44x44Logo.scale-240.png",
198 |             "sizes": "106x106"
199 |         },
200 |         ...
201 |     ],
202 |     "scope": "/racer/",
203 |     "start_url": "http://www.racer2k.net/racer/start.html",
204 |     "display": "fullscreen",
205 |     "orientation": "portrait"
206 | }
207 | 
208 | 209 | ### Navigation Scope 210 | For a hosted web application, the W3C manifest defines a scope that restricts the URLs to which the application can navigate. Additionally, the manifest can include a proprietary setting named **mjs_extended_scope** that defines an array of scope rules each one indicating whether URLs matching the rule should be navigated to by the application. Non-matching URLs will be launched externally. 211 | 212 | Typically, Cordova applications define scope rules to implement a security policy that controls access to external domains. To configure the security policy, the plugin hook maps the scope rules in the W3C manifest (**manifest.json**) to suitable `` elements in the Cordova configuration file (**config.xml**). For example: 213 | 214 | **Manifest.json** 215 |
216 | ...
217 |    "start_url": "http://www.xyz.com/",
218 |    "scope":  "/", 
219 |    "mjs_extended_scope": [
220 |      { "url": "http//otherdomain.com/*" },
221 |      { "url": "http//login.anotherdomain.com/" }
222 |    ]
223 | ...
224 | 
225 | 226 | **Config.xml** 227 |
228 | ...
229 | <allow-navigation href="http://www.xyz.com/*" />
230 | <allow-navigation href="http://otherdomain.com/*" /> 
231 | <allow-navigation href="http://login.anotherdomain.com/" />
232 | ...
233 | 
234 | 235 | ## Methods 236 | Even though the following methods are available, it should be pointed out that calling them is not required as the plugin will provide most of its functionality by simply embedding a W3C manifest in the application package. 237 | 238 | ### loadManifest 239 | Loads the specified W3C manifest. 240 | 241 | `hostedwebapp.loadManifest(successCallback, errorCallback, manifestFileName)` 242 | 243 | |**Parameter** |**Description** | 244 | |:-----------------|:--------------------------------------------------------------------------| 245 | |_successCallback_ |A callback that is passed a manifest object. | 246 | |_errorCallback_ |A callback that executes if an error occurs when loading the manifest file.| 247 | |_manifestFileName_|The name of the manifest file to load. | 248 | 249 | ### getManifest 250 | Returns the currently loaded manifest. 251 | 252 | `hostedwebapp.getManifest(successCallback, errorCallback)` 253 | 254 | |**Parameter** |**Description** | 255 | |:-----------------|:--------------------------------------------------------------------------| 256 | |_successCallback_ |A callback that is passed a manifest object. | 257 | |_errorCallback_ |A callback that executes if a manifest is not currently available. | 258 | 259 | ### enableOfflinePage 260 | Enables offline page support. 261 | 262 | `hostedwebapp.enableOfflinePage()` 263 | 264 | ### disableOfflinePage 265 | Disables offline page support. 266 | 267 | `hostedwebapp.disableOfflinePage()` 268 | 269 | ## Supported Platforms 270 | Windows 8.1 271 | Windows Phone 8.1 272 | iOS 273 | Android 274 | 275 | ### Windows and Windows Phone Quirks 276 | 277 | Cordova for Android and iOS platforms provide a security policy to control which network requests triggered by the page (css, js, images, XHRs, etc.) are allowed to be made; this means that they will be blocked if they don't match the `origin` attribute of any of the `` elements defined in the Cordova configuration file (**config.xml**). 278 | 279 | The Windows and Windows Phone platforms do not provide control for these kind of requests, and they will be allowed. 280 | 281 | ## Changelog 282 | 283 | Releases are documented in [GitHub](https://github.com/manifoldjs/ManifoldCordova/releases). 284 | -------------------------------------------------------------------------------- /scripts/createConfigParser.js: -------------------------------------------------------------------------------- 1 | module.exports = function (xml, etree, ConfigParser) { 2 | var config = new ConfigParser(xml); 3 | 4 | // set the text for an element 5 | config.setElement = function (name, text) { 6 | if (text) { 7 | var el = this.doc.find(name); 8 | if (!el) { 9 | var root = this.doc.getroot(); 10 | el = new etree.SubElement(root, name); 11 | } 12 | 13 | el.text = text; 14 | } 15 | }; 16 | 17 | // TODO: replace this 18 | config.setAttribute = function (elname, attname, value) { 19 | if (value) { 20 | var el = this.doc.find(elname); 21 | if (!el) { 22 | var root = this.doc.getroot(); 23 | el = new etree.SubElement(root, elname); 24 | } 25 | 26 | el.set(attname, value); 27 | } 28 | }; 29 | 30 | // set the value of a "preference" element 31 | config.setPreference = function (name, value) { 32 | if (value) { 33 | var el = this.doc.find('preference[@name=\'' + name + '\']'); 34 | if (!el) { 35 | var root = this.doc.getroot(); 36 | el = new etree.SubElement(root, 'preference'); 37 | el.set('name', name); 38 | } 39 | 40 | el.set('value', value); 41 | } 42 | }; 43 | 44 | // get all elements with the specified name 45 | config.getElements = function (name) { 46 | return this.doc.findall(name); 47 | }; 48 | 49 | // remove all elements from the document matching the specified XPath expression 50 | config.removeElements = function (path){ 51 | var removeChilds = function (childs, elements) { 52 | for(var i=0; i < elements.length; i++){ 53 | var idx = childs.indexOf(elements[i]); 54 | if(idx > -1){ 55 | childs.splice(idx,1); 56 | } 57 | } 58 | 59 | childs.forEach(function (child) { 60 | removeChilds(child.getchildren(), elements); 61 | }); 62 | } 63 | 64 | var elements = this.doc.findall(path); 65 | removeChilds(this.doc.getroot().getchildren(), elements); 66 | }; 67 | 68 | return config; 69 | } 70 | -------------------------------------------------------------------------------- /scripts/downloader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var http = require('http'); 4 | var https = require('https'); 5 | var url = require('url'); 6 | 7 | var path = require('path'); 8 | var fs = require('fs'); 9 | 10 | var downloadImage = function (inputUri, downloadDir, callback) { 11 | var uri = url.parse(inputUri); 12 | 13 | if (inputUri.indexOf('http://') !== 0 && inputUri.indexOf('https://') !== 0) { 14 | // this is to detect scenarios like localhost:8080 where localhost is 15 | // treated as protocol even if it's not. 16 | if (inputUri.indexOf(uri.protocol + '//') !== 0) { 17 | inputUri = 'http://' + inputUri; 18 | uri = url.parse(inputUri); 19 | } 20 | } 21 | 22 | if(!(uri.protocol === 'http:' || uri.protocol === 'https:')) { 23 | return callback(new Error('Invalid protocol, only http & https are supported')); 24 | } 25 | 26 | if (!fs.existsSync(downloadDir)) { 27 | return callback(new Error('Invalid download directory: ' + downloadDir)); 28 | } 29 | 30 | var fileName = uri.pathname.split('/').pop(); 31 | var filePath = path.join(downloadDir, fileName); 32 | 33 | var lastModified; 34 | 35 | if (fs.existsSync(filePath)) { 36 | var stats = fs.lstatSync(filePath); 37 | lastModified = new Date(stats.mtime); 38 | } 39 | 40 | var options = { 41 | host: uri.hostname, 42 | port: uri.port || (uri.protocol === 'https:' ? 443 : 80), 43 | path: uri.path, 44 | agent : false 45 | }; 46 | 47 | if (lastModified) { 48 | options.headers = { 49 | 'if-modified-since': lastModified.toUTCString() 50 | } 51 | } 52 | 53 | var protocol = uri.protocol === 'https:' ? https : http; 54 | protocol.get(options, function(res) { 55 | // If Moved Permanently or Found, redirect to new URL 56 | if ([301, 302].indexOf(res.statusCode) > -1) { 57 | return downloadImage(res.headers.location, downloadDir, callback); 58 | } 59 | 60 | // If not OK or Not Modified, throw error 61 | if ([200, 304].indexOf(res.statusCode) === -1) { 62 | return callback(new Error('Invalid status code: ' + res.statusCode + ' - ' + res.statusMessage)) 63 | } 64 | 65 | // If Not Modified, ignore 66 | if (res.statusCode === 304) { 67 | return callback(undefined, { 'path': filePath, 'statusCode': res.statusCode, 'statusMessage': res.statusMessage }); 68 | } 69 | 70 | // If not an image, throw error 71 | if (!res.headers['content-type'].match(/image/)) { 72 | return callback(new Error('Unexpected Content-Type: ' + res.headers['content-type'])) 73 | } 74 | 75 | // Else save 76 | res.pipe(fs.createWriteStream(filePath)) 77 | .on('close', function () { 78 | var lastAccessed = new Date(); 79 | var lastModified = res.headers['last-modified'] ? new Date(res.headers['last-modified']) : lastAccessed; 80 | 81 | // update the last modified time of the file to match the response header 82 | fs.utimes(filePath, lastAccessed, lastModified, function (err) { 83 | return callback(err, { 'path': filePath, 'statusCode': res.statusCode, 'statusMessage': res.statusMessage }); 84 | }); 85 | }); 86 | }).on('error', function(err) { 87 | return callback(err); 88 | }); 89 | } 90 | 91 | module.exports = { 92 | downloadImage: downloadImage, 93 | } 94 | -------------------------------------------------------------------------------- /scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scripts", 3 | "version": "1.0.0", 4 | "description": "", 5 | "directories": { 6 | "test": "test" 7 | }, 8 | "scripts": { 9 | "test": "node ./node_modules/mocha/bin/mocha", 10 | "builder": "node ./node_modules/mocha/bin/mocha --reporter mocha-teamcity-reporter" 11 | }, 12 | "devDependencies": { 13 | "cordova-lib": "^5.4.0", 14 | "mocha": "^2.2.1", 15 | "mocha-teamcity-reporter": "0.0.4", 16 | "q": "^1.2.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /scripts/replaceWindowsWrapperFiles.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var fs = require('fs'), 4 | path = require('path'), 5 | url = require('url'), 6 | etree, 7 | projectRoot; 8 | 9 | var logger = { 10 | log: function () { 11 | if (process.env.NODE_ENV !== 'test') { 12 | console.log.apply(this, arguments) 13 | } 14 | }, 15 | warn: function() { 16 | if (process.env.NODE_ENV !== 'test') { 17 | console.warn.apply(this, arguments) 18 | } 19 | } 20 | }; 21 | 22 | function copyFile(source, target, callback) { 23 | var cbCalled = false; 24 | 25 | function done(err) { 26 | if (!cbCalled) { 27 | callback(err); 28 | cbCalled = true; 29 | } 30 | } 31 | 32 | var rd = fs.createReadStream(source); 33 | rd.on('error', done); 34 | 35 | var wr = fs.createWriteStream(target); 36 | wr.on('error', done); 37 | wr.on('close', function() { 38 | done(); 39 | }); 40 | rd.pipe(wr); 41 | }; 42 | 43 | function updateManifestFile(manifestPath) { 44 | var contents = fs.readFileSync(manifestPath, 'utf-8'); 45 | if(contents) { 46 | //Windows is the BOM. Skip the Byte Order Mark. 47 | contents = contents.substring(contents.indexOf('<')); 48 | } 49 | 50 | var startPage = "www/wrapper.html"; 51 | var manifest = new etree.ElementTree(etree.XML(contents)); 52 | var appNode = manifest.find('.//Application'); 53 | 54 | appNode.attrib.StartPage = startPage; 55 | 56 | // Write out manifest 57 | fs.writeFileSync(manifestPath, manifest.write({indent: 4}), 'utf-8'); 58 | } 59 | 60 | function updateWindowsManifests() { 61 | var MANIFEST_WINDOWS8 = 'package.windows80.appxmanifest', 62 | MANIFEST_WINDOWS = 'package.windows.appxmanifest', 63 | MANIFEST_PHONE = 'package.phone.appxmanifest', 64 | MANIFEST_WINDOWS10 = 'package.windows10.appxmanifest'; 65 | 66 | // Apply appxmanifest changes 67 | [ MANIFEST_WINDOWS, 68 | MANIFEST_WINDOWS8, 69 | MANIFEST_PHONE, 70 | MANIFEST_WINDOWS10 ].forEach( 71 | function(manifestFile) { 72 | updateManifestFile(path.join(projectRoot, "platforms", "windows", manifestFile)); 73 | }); 74 | } 75 | 76 | module.exports = function (context) { 77 | projectRoot = context.opts.projectRoot; 78 | 79 | // if the windows folder does not exist, cancell the script 80 | var windowsPath = path.join(projectRoot, "platforms","windows"); 81 | if (!fs.existsSync(windowsPath)) { 82 | return; 83 | } 84 | 85 | etree = context.requireCordovaModule('cordova-lib/node_modules/elementtree'); 86 | 87 | // move contents of the assets folder to the windows platform dir 88 | var Q = context.requireCordovaModule('q'); 89 | 90 | var filename = "wrapper"; 91 | 92 | var sourcePath = path.resolve(__dirname, "..", "assets", "windows", "wrapper.html"); 93 | var destPath = path.join(projectRoot, "platforms","windows", "www", filename + ".html"); 94 | 95 | logger.log('Copying wrapper html file for the windows platform from '+ sourcePath + ' to ' + destPath + '.'); 96 | 97 | var task = Q.defer(); 98 | copyFile(sourcePath, destPath, function (err) { 99 | if (err) { 100 | console.error(err); 101 | return task.reject(err); 102 | } 103 | 104 | console.log("Finished copying wrapper html file for the windows platform."); 105 | 106 | var sourcePath = path.resolve(__dirname, "..", "assets", "windows", "wrapper.js"); 107 | var destPath = path.join(projectRoot, "platforms", "windows", "www", "js", filename +".js"); 108 | 109 | logger.log('Copying wrapper js file for the windows platform from '+ sourcePath + ' to ' + destPath + '.'); 110 | 111 | copyFile(sourcePath, destPath, function (err) { 112 | if (err) { 113 | console.error(err); 114 | return task.reject(err); 115 | } 116 | 117 | console.log("Finished copying wrapper js file for the windows platform."); 118 | 119 | var sourcePath = path.resolve(__dirname, "..", "assets", "windows", "wrapper.css"); 120 | var destPath = path.join(projectRoot, "platforms", "windows", "www", "css", filename + ".css"); 121 | 122 | logger.log('Copying wrapper css file for the windows platform from '+ sourcePath + ' to ' + destPath + '.'); 123 | 124 | copyFile(sourcePath, destPath, function (err) { 125 | if (err) { 126 | console.error(err); 127 | return task.reject(err); 128 | } 129 | 130 | console.log("Finished copying wrapper css file for the windows platform."); 131 | 132 | updateWindowsManifests(); 133 | 134 | task.resolve(); 135 | }); 136 | }); 137 | }); 138 | 139 | return task.promise; 140 | }; 141 | -------------------------------------------------------------------------------- /scripts/rollbackWindowsWrapperFiles.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var createConfigParser = require('./createConfigParser'), 4 | fs = require('fs'), 5 | path = require('path'), 6 | url = require('url'), 7 | pendingTasks = [], 8 | Q, 9 | config, 10 | projectRoot, 11 | etree; 12 | 13 | var logger = { 14 | log: function () { 15 | if (process.env.NODE_ENV !== 'test') { 16 | console.log.apply(this, arguments) 17 | } 18 | }, 19 | warn: function() { 20 | if (process.env.NODE_ENV !== 'test') { 21 | console.warn.apply(this, arguments) 22 | } 23 | } 24 | }; 25 | 26 | function deleteFile(path) { 27 | var t = Q.defer(); 28 | pendingTasks.push(t); 29 | 30 | logger.log('Deleting ' + path + ' file for the windows platform.'); 31 | 32 | fs.unlink(path, function (err) { 33 | if (err) { 34 | console.log(err); 35 | return t.reject(); 36 | } 37 | 38 | t.resolve(); 39 | }); 40 | } 41 | 42 | // Configure Cordova configuration parser 43 | function configureParser(context) { 44 | var cordova_util = context.requireCordovaModule('cordova-lib/src/cordova/util'); 45 | var ConfigParser; 46 | try { 47 | ConfigParser = context.requireCordovaModule('cordova-lib/node_modules/cordova-common').ConfigParser; 48 | } catch (err) { 49 | // Fallback to old location of config parser (old versions of cordova-lib) 50 | ConfigParser = context.requireCordovaModule('cordova-lib/src/configparser/ConfigParser'); 51 | } 52 | 53 | etree = context.requireCordovaModule('cordova-lib/node_modules/elementtree'); 54 | 55 | var xml = cordova_util.projectConfig(context.opts.projectRoot); 56 | config = createConfigParser(xml, etree, ConfigParser); 57 | } 58 | 59 | module.exports = function (context) { 60 | // If the plugin is not being removed, cancel the script 61 | if (context.opts.plugins.indexOf(context.opts.plugin.id) == -1) { 62 | return; 63 | } 64 | 65 | var projectRoot = context.opts.projectRoot; 66 | 67 | // if the windows folder does not exist, cancell the script 68 | var windowsPath = path.join(projectRoot, "platforms","windows"); 69 | if (!fs.existsSync(windowsPath)) { 70 | return; 71 | } 72 | 73 | Q = context.requireCordovaModule('q'); 74 | var task = Q.defer(); 75 | 76 | var destPath = path.join(projectRoot, "platforms", "windows", "www", "wrapper.html"); 77 | if (fs.existsSync(destPath)) { 78 | deleteFile(destPath); 79 | } 80 | 81 | destPath = path.join(projectRoot, "platforms", "windows", "www", "js", "wrapper.js"); 82 | 83 | if (fs.existsSync(destPath)) { 84 | deleteFile(destPath); 85 | } 86 | 87 | destPath = path.join(projectRoot, "platforms", "windows", "www", "css", "wrapper.css"); 88 | 89 | if (fs.existsSync(destPath)) { 90 | deleteFile(destPath); 91 | } 92 | 93 | Q.allSettled(pendingTasks).then(function (e) { 94 | console.log("Finished removing assets for the windows platform."); 95 | 96 | // restore content source to index.html in all platforms. 97 | configureParser(context); 98 | if (config) { 99 | console.log("Restoring content source value to index.html"); 100 | config.setAttribute('content', 'src', 'index.html'); 101 | config.write(); 102 | } 103 | else { 104 | console.log("could not load config.xml file"); 105 | } 106 | 107 | task.resolve(); 108 | }); 109 | 110 | return task.promise; 111 | }; 112 | -------------------------------------------------------------------------------- /scripts/test/assets/fullAccessRules/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | HelloWorld 4 | 5 | A sample Apache Cordova application that responds to the deviceready event. 6 | 7 | 8 | Apache Cordova Team 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /scripts/test/assets/fullAccessRules/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "start_url": "http://wat-docs.azurewebsites.net/", 3 | "name": "WAT Documentation", 4 | "orientation": "landscape", 5 | "display": "fullscreen", 6 | "scope": "http://wat-docs.azurewebsites.net/*", 7 | "mjs_extended_scope": [ 8 | "http://wat.codeplex.com" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /scripts/test/assets/fullAccessRules/www/empty.txt: -------------------------------------------------------------------------------- 1 | empty folder -------------------------------------------------------------------------------- /scripts/test/assets/fullUrlForScope/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /scripts/test/assets/fullUrlForScope/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "start_url": "http://wat-docs.azurewebsites.net/", 3 | "name": "WAT Documentation", 4 | "orientation": "landscape", 5 | "display": "fullscreen", 6 | "scope": "http://www.domain.com" 7 | } 8 | -------------------------------------------------------------------------------- /scripts/test/assets/fullUrlForScope/www/empty.txt: -------------------------------------------------------------------------------- 1 | empty folder -------------------------------------------------------------------------------- /scripts/test/assets/jsonPropertiesMissing/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | HelloWorld 4 | 5 | A sample Apache Cordova application that responds to the deviceready event. 6 | 7 | 8 | Apache Cordova Team 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /scripts/test/assets/jsonPropertiesMissing/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "start_url": "http://wat-docs.azurewebsites.net/" 3 | } 4 | -------------------------------------------------------------------------------- /scripts/test/assets/jsonPropertiesMissing/www/empty.txt: -------------------------------------------------------------------------------- 1 | empty folder -------------------------------------------------------------------------------- /scripts/test/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwa-builder/ManifoldCordova/268a4e5c81ff6b8bde028ea56651372708da90c5/scripts/test/assets/logo.png -------------------------------------------------------------------------------- /scripts/test/assets/normalFlow/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | HelloWorld 4 | 5 | A sample Apache Cordova application that responds to the deviceready event. 6 | 7 | 8 | Apache Cordova Team 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /scripts/test/assets/normalFlow/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "start_url": "http://wat-docs.azurewebsites.net/", 3 | "short_name": "WAT Docs", 4 | "name": "WAT Documentation", 5 | "orientation": "landscape", 6 | "display": "fullscreen" 7 | } 8 | -------------------------------------------------------------------------------- /scripts/test/assets/normalFlow/www/empty.txt: -------------------------------------------------------------------------------- 1 | empty folder -------------------------------------------------------------------------------- /scripts/test/assets/shortNameMissing/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | HelloWorld 4 | 5 | A sample Apache Cordova application that responds to the deviceready event. 6 | 7 | 8 | Apache Cordova Team 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /scripts/test/assets/shortNameMissing/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "start_url": "http://wat-docs.azurewebsites.net/", 3 | "name": "WAT Documentation", 4 | "orientation": "landscape", 5 | "display": "fullscreen" 6 | } 7 | -------------------------------------------------------------------------------- /scripts/test/assets/shortNameMissing/www/empty.txt: -------------------------------------------------------------------------------- 1 | empty folder -------------------------------------------------------------------------------- /scripts/test/assets/shortNameWithSlashes/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | HelloWorld 4 | 5 | A sample Apache Cordova application that responds to the deviceready event. 6 | 7 | 8 | Apache Cordova Team 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /scripts/test/assets/shortNameWithSlashes/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "start_url": "http://wat-docs.azurewebsites.net/", 3 | "short_name": "WAT /Docs/", 4 | "name": "WAT Documentation", 5 | "orientation": "landscape", 6 | "display": "fullscreen" 7 | } 8 | -------------------------------------------------------------------------------- /scripts/test/assets/shortNameWithSlashes/www/empty.txt: -------------------------------------------------------------------------------- 1 | empty folder -------------------------------------------------------------------------------- /scripts/test/assets/wildcardSubdomainForScope/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /scripts/test/assets/wildcardSubdomainForScope/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "start_url": "http://wat-docs.azurewebsites.net/", 3 | "name": "WAT Documentation", 4 | "orientation": "landscape", 5 | "display": "fullscreen", 6 | "scope": "http://*.domain.com" 7 | } 8 | -------------------------------------------------------------------------------- /scripts/test/assets/wildcardSubdomainForScope/www/empty.txt: -------------------------------------------------------------------------------- 1 | empty folder -------------------------------------------------------------------------------- /scripts/test/assets/xmlEmptyWidget/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /scripts/test/assets/xmlEmptyWidget/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "start_url": "http://wat-docs.azurewebsites.net/", 3 | "name": "WAT Documentation", 4 | "orientation": "landscape", 5 | "display": "fullscreen", 6 | "scope": "/scope-path/", 7 | "mjs_extended_scope": [ 8 | "whitelist-rule-1", 9 | "whitelist-rule-2" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /scripts/test/assets/xmlEmptyWidget/www/empty.txt: -------------------------------------------------------------------------------- 1 | empty folder -------------------------------------------------------------------------------- /scripts/test/downloader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | process.env.NODE_ENV = 'test'; 3 | 4 | var path = require('path'); 5 | var fs = require('fs'); 6 | 7 | var http = require('http'); 8 | var https = require('https'); 9 | 10 | var url = require('url'); 11 | 12 | var assert = require('assert'); 13 | 14 | var downloader = require('../downloader'); 15 | 16 | var tu = require('./test-utils'); 17 | 18 | var assetsDirectory = path.join(__dirname, 'assets'); 19 | var tmpDirectory = path.join(__dirname, 'tmp'); 20 | 21 | var responseFunction; 22 | 23 | var server = http.createServer(function (req, res) { 24 | if (responseFunction) { 25 | responseFunction(req, res); 26 | } else { 27 | var fileName = url.parse(req.url).pathname.split('/').pop(); 28 | var filePath = path.join(assetsDirectory, fileName); 29 | 30 | if (!fs.existsSync(filePath)) { 31 | res.writeHead(404); 32 | res.end(); 33 | } 34 | 35 | res.writeHead(200, { 'content-type':'image/png' }); 36 | fs.createReadStream(filePath) 37 | .pipe(res); 38 | } 39 | }); 40 | 41 | describe('downloader module', function () { 42 | describe('downloadImage()', function () { 43 | before(function () { 44 | server.listen(8042); 45 | }); 46 | 47 | beforeEach(function () { 48 | if (!fs.existsSync(tmpDirectory)) { 49 | fs.mkdirSync(tmpDirectory); 50 | } 51 | }); 52 | 53 | it('Should return an Error if download path is invalid', function (done) { 54 | downloader.downloadImage('http://localhost:8042/logo.png', undefined, function(err, data) { 55 | assert(err); 56 | done(); 57 | }); 58 | }); 59 | 60 | it('Should return an Error if server returns invalid status code', function (done) { 61 | responseFunction = function(req, res) { 62 | res.writeHead(404); 63 | res.end(); 64 | }; 65 | 66 | downloader.downloadImage('http://localhost:8042/logo.png', tmpDirectory, function(err, data) { 67 | assert(err); 68 | done(); 69 | }); 70 | }); 71 | 72 | it('Should return an Error if content type is not an image', function (done) { 73 | responseFunction = function(req, res) { 74 | res.writeHead(200, {'content-type':'text/html'}); 75 | res.end(); 76 | }; 77 | 78 | downloader.downloadImage('http://localhost:8042/logo.png', tmpDirectory, function(err, data) { 79 | assert(err); 80 | done(); 81 | }); 82 | }); 83 | 84 | it('Should take image name from url', function (done) { 85 | var expectedFileName = 'logo.png'; 86 | 87 | downloader.downloadImage('http://localhost:8042/logo.png', tmpDirectory, function(err, data) { 88 | assert(data.path); 89 | assert(data.path === path.join(tmpDirectory, expectedFileName)); 90 | done(); 91 | }); 92 | }); 93 | 94 | it('Should save image in download directory', function (done) { 95 | downloader.downloadImage('http://localhost:8042/logo.png', tmpDirectory, function(err, data) { 96 | assert(fs.existsSync(data.path)); 97 | done(); 98 | }); 99 | }); 100 | 101 | it('Should replace image in download directory', function (done) { 102 | downloader.downloadImage('http://localhost:8042/logo.png', tmpDirectory, function(err, data) { 103 | downloader.downloadImage('http://localhost:8042/logo.png', tmpDirectory, function(err, data) { 104 | assert(fs.existsSync(data.path)); 105 | done(); 106 | }); 107 | }); 108 | }); 109 | 110 | it('Should download an image if a redirect response is received', function (done) { 111 | responseFunction = function(req, res) { 112 | if (req.url.indexOf('redirect') > -1) { 113 | res.writeHead(302, {'location': 'http://localhost:8042/logo.png'}); 114 | res.end(); 115 | } else { 116 | var fileName = url.parse(req.url).pathname.split('/').pop(); 117 | var filePath = path.join(assetsDirectory, fileName); 118 | 119 | if (!fs.existsSync(filePath)) { 120 | res.writeHead(404); 121 | res.end(); 122 | } 123 | 124 | res.writeHead(200, {'content-type':'image/png'}); 125 | fs.createReadStream(filePath) 126 | .pipe(res); 127 | } 128 | }; 129 | 130 | var expectedFileName = 'logo.png'; 131 | 132 | downloader.downloadImage('http://localhost:8042/redirect', tmpDirectory, function(err, data) { 133 | assert(data.path); 134 | assert(data.path === path.join(tmpDirectory, expectedFileName)); 135 | assert(fs.existsSync(data.path)); 136 | done(); 137 | }); 138 | }); 139 | 140 | it('Should not download an image if a Not Modified response is received', function (done) { 141 | downloader.downloadImage('http://localhost:8042/logo.png', tmpDirectory, function(err, data) { 142 | responseFunction = function(req, res) { 143 | res.writeHead(304); 144 | res.end(); 145 | }; 146 | 147 | var expectedLastModified = new Date(fs.lstatSync(data.path).mtime); 148 | assert(data.path); 149 | 150 | downloader.downloadImage('http://localhost:8042/logo.png', tmpDirectory, function(err, data) { 151 | var actualLastModified = new Date(fs.lstatSync(data.path).mtime); 152 | assert(data.path); 153 | assert(fs.existsSync(data.path)); 154 | assert(expectedLastModified.toUTCString() === actualLastModified.toUTCString()); 155 | done(); 156 | }); 157 | }); 158 | }); 159 | 160 | it('Should download an image if protocol is https', function (done) { 161 | // set timeout to 20 seconds as we are downloading a file from internet. 162 | this.timeout(20 * 1000); 163 | 164 | var fileName = 'Wiki.png'; 165 | var filePath = path.join(assetsDirectory, fileName); 166 | 167 | downloader.downloadImage('https://upload.wikimedia.org/wikipedia/en/b/bc/Wiki.png', tmpDirectory, function(err, data) { 168 | assert(fs.existsSync(data.path)); 169 | done(); 170 | }); 171 | }); 172 | 173 | it('Should return an Error if invalid protocol is provided', function (done) { 174 | downloader.downloadImage('ftp://localhost:8042/logo.png', tmpDirectory, function(err, data) { 175 | assert(err); 176 | done(); 177 | }); 178 | }); 179 | 180 | it('Should use http as protocol by default', function (done) { 181 | downloader.downloadImage('localhost:8042/logo.png', tmpDirectory, function(err, data) { 182 | assert(fs.existsSync(data.path)); 183 | done(); 184 | }); 185 | }); 186 | 187 | afterEach(function () { 188 | responseFunction = undefined; 189 | tu.deleteRecursiveSync(tmpDirectory); 190 | }); 191 | 192 | after(function () { 193 | server.close(); 194 | }); 195 | }); 196 | }); 197 | -------------------------------------------------------------------------------- /scripts/test/readme.md: -------------------------------------------------------------------------------- 1 | # Running the tests 2 | The tests are built with mocha, to run them: 3 | 4 | 1. Install mocha with the following command `npm install -g mocha` 5 | 1. Install the dependencies with the following command `npm install` in the scripts directory 6 | 1. Make sure you are on the scripts directory and invoke mocha `mocha` 7 | -------------------------------------------------------------------------------- /scripts/test/test-utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var fs = require('fs'); 5 | 6 | var copyRecursiveSync = function (src, dest) { 7 | if (!fs.existsSync(src)) { return; } 8 | if (fs.lstatSync(src).isDirectory()) { 9 | if (!fs.existsSync(dest)) { fs.mkdirSync(dest); } 10 | fs.readdirSync(src).forEach(function(childItemName) { 11 | copyRecursiveSync(path.join(src, childItemName), 12 | path.join(dest, childItemName)); 13 | }); 14 | } else { 15 | fs.writeFileSync(dest, fs.readFileSync(src)); 16 | } 17 | }; 18 | 19 | var deleteRecursiveSync = function(src) { 20 | if (!fs.existsSync(src)) { return; } 21 | if (fs.lstatSync(src).isDirectory()) { 22 | fs.readdirSync(src).forEach(function(childItemName) { 23 | deleteRecursiveSync(path.join(src, childItemName)) 24 | }); 25 | fs.rmdirSync(src); 26 | } else { 27 | fs.unlinkSync(src); 28 | } 29 | }; 30 | 31 | module.exports = { 32 | copyRecursiveSync : copyRecursiveSync, 33 | deleteRecursiveSync : deleteRecursiveSync 34 | } 35 | -------------------------------------------------------------------------------- /scripts/test/updateConfigurationBeforePrepare.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | process.env.NODE_ENV = 'test'; 3 | 4 | var updateConfiguration = require('../updateConfigurationBeforePrepare'); 5 | var tu = require('./test-utils'); 6 | 7 | var assert = require('assert'); 8 | 9 | var path = require('path'); 10 | var fs = require('fs'); 11 | 12 | var assetsDirectory = path.join(__dirname, 'assets'); 13 | var workingDirectory = path.join(__dirname, 'tmp'); 14 | 15 | function initializeContext(testDir) { 16 | 17 | var ctx = { 18 | opts : { 19 | plugin: { 20 | id: 'cordova-plugin-hostedwebapp' 21 | }, 22 | projectRoot : testDir 23 | } 24 | }; 25 | 26 | var requireCordovaModule = ctx.requireCordovaModule; 27 | 28 | ctx.requireCordovaModule = function(moduleName) { 29 | if (!moduleName) { 30 | if (requireCordovaModule) { 31 | return requireCordovaModule(moduleName); 32 | } 33 | else { 34 | return; 35 | } 36 | } 37 | 38 | if (moduleName === 'q') { 39 | return require('q'); 40 | } 41 | 42 | if (moduleName === 'cordova-lib/src/cordova/util') { 43 | return require('cordova-lib/src/cordova/util'); 44 | } 45 | 46 | if (moduleName === 'cordova-lib/node_modules/cordova-common') { 47 | return require('cordova-lib/node_modules/cordova-common'); 48 | } 49 | 50 | if (moduleName === 'cordova-lib/src/configparser/ConfigParser') { 51 | return require('cordova-lib/src/configparser/ConfigParser'); 52 | } 53 | 54 | if (moduleName === 'cordova-lib/node_modules/elementtree') { 55 | return require('cordova-lib/node_modules/elementtree'); 56 | } 57 | 58 | if (requireCordovaModule) { 59 | return requireCordovaModule(moduleName); 60 | } 61 | }; 62 | 63 | return ctx; 64 | } 65 | 66 | describe('updateConfigurationBeforePrepare.js', function (){ 67 | beforeEach(function () { 68 | tu.copyRecursiveSync(assetsDirectory, workingDirectory); 69 | }); 70 | 71 | it('Should update name with short_name value (without spaces) from manifest.json', function (done){ 72 | var testDir = path.join(workingDirectory, 'normalFlow'); 73 | var configXML = path.join(testDir, 'config.xml'); 74 | var ctx = initializeContext(testDir); 75 | 76 | updateConfiguration(ctx).then(function() { 77 | var content = fs.readFileSync(configXML).toString(); 78 | assert(content.indexOf('WATDocs') > -1); 79 | 80 | done(); 81 | }); 82 | }); 83 | 84 | it('Should update name with name value (without spaces) from manifest.json if short_name is missing', function (done){ 85 | var testDir = path.join(workingDirectory, 'shortNameMissing'); 86 | var configXML = path.join(testDir, 'config.xml'); 87 | var ctx = initializeContext(testDir); 88 | 89 | updateConfiguration(ctx).then(function() { 90 | var content = fs.readFileSync(configXML).toString(); 91 | assert(content.indexOf('WATDocumentation') > -1); 92 | 93 | done(); 94 | }); 95 | }); 96 | 97 | it('Should ignore slashes when updating name from manifest.json', function (done) { 98 | var testDir = path.join(workingDirectory, 'shortNameWithSlashes'); 99 | var configXML = path.join(testDir, 'config.xml'); 100 | var ctx = initializeContext(testDir); 101 | 102 | updateConfiguration(ctx).then(function () { 103 | var content = fs.readFileSync(configXML).toString(); 104 | assert(content.indexOf('WATDocs') > -1); 105 | 106 | done(); 107 | }); 108 | }); 109 | 110 | it('Should not update name if it is missing in manifest.json', function (done) { 111 | var testDir = path.join(workingDirectory, 'jsonPropertiesMissing'); 112 | var configXML = path.join(testDir, 'config.xml'); 113 | var ctx = initializeContext(testDir); 114 | 115 | updateConfiguration(ctx).then(function () { 116 | var content = fs.readFileSync(configXML).toString(); 117 | assert(content.indexOf('HelloWorld') > -1); 118 | 119 | done(); 120 | }); 121 | }); 122 | 123 | it('Should add name if XML element is missing', function (done){ 124 | var testDir = path.join(workingDirectory, 'xmlEmptyWidget'); 125 | var configXML = path.join(testDir, 'config.xml'); 126 | var ctx = initializeContext(testDir); 127 | 128 | updateConfiguration(ctx).then(function () { 129 | var content = fs.readFileSync(configXML).toString(); 130 | assert(content.indexOf('WATDocumentation') > content.indexOf('')); 131 | assert(content.indexOf('WATDocumentation') < content.indexOf('')); 132 | done(); 133 | }); 134 | }); 135 | 136 | 137 | it('Should update orientation with value from manifest.json', function (done){ 138 | var testDir = path.join(workingDirectory, 'normalFlow'); 139 | var configXML = path.join(testDir, 'config.xml'); 140 | var ctx = initializeContext(testDir); 141 | 142 | updateConfiguration(ctx).then(function () { 143 | var content = fs.readFileSync(configXML).toString(); 144 | assert(content.indexOf('') > -1); 145 | 146 | done(); 147 | }); 148 | 149 | 150 | }); 151 | 152 | it('Should not update orientation if it is missing in manifest.json', function (done){ 153 | var testDir = path.join(workingDirectory, 'jsonPropertiesMissing'); 154 | var configXML = path.join(testDir, 'config.xml'); 155 | var ctx = initializeContext(testDir); 156 | 157 | updateConfiguration(ctx).then(function () { 158 | var content = fs.readFileSync(configXML).toString(); 159 | assert(content.indexOf('') > content.indexOf('')); 160 | assert(content.indexOf('') < content.indexOf('')); 161 | 162 | done(); 163 | }); 164 | }); 165 | 166 | it('Should add orientation if XML element element is missing', function (done){ 167 | var testDir = path.join(workingDirectory, 'xmlEmptyWidget'); 168 | var configXML = path.join(testDir, 'config.xml'); 169 | var ctx = initializeContext(testDir); 170 | 171 | updateConfiguration(ctx).then(function () { 172 | var content = fs.readFileSync(configXML).toString(); 173 | assert(content.indexOf('') > content.indexOf('')); 174 | assert(content.indexOf('') < content.indexOf('')); 175 | 176 | done(); 177 | }); 178 | }); 179 | 180 | it('Should update fullscreen with value from manifest.json', function (done){ 181 | var testDir = path.join(workingDirectory, 'normalFlow'); 182 | var configXML = path.join(testDir, 'config.xml'); 183 | var ctx = initializeContext(testDir); 184 | 185 | updateConfiguration(ctx).then(function () { 186 | var content = fs.readFileSync(configXML).toString(); 187 | assert(content.indexOf('') > -1); 188 | 189 | done(); 190 | }); 191 | }); 192 | 193 | it('Should not update fullscreen if it is missing in manifest.json', function (done){ 194 | var testDir = path.join(workingDirectory, 'jsonPropertiesMissing'); 195 | var configXML = path.join(testDir, 'config.xml'); 196 | var ctx = initializeContext(testDir); 197 | 198 | updateConfiguration(ctx).then(function () { 199 | var content = fs.readFileSync(configXML).toString(); 200 | assert(content.indexOf('') > -1); 201 | 202 | done(); 203 | }); 204 | }); 205 | 206 | it('Should add fullscreen if XML element is missing', function (done){ 207 | var testDir = path.join(workingDirectory, 'xmlEmptyWidget'); 208 | var configXML = path.join(testDir, 'config.xml'); 209 | var ctx = initializeContext(testDir); 210 | 211 | updateConfiguration(ctx).then(function () { 212 | var content = fs.readFileSync(configXML).toString(); 213 | assert(content.indexOf('') > content.indexOf('')); 214 | assert(content.indexOf('') < content.indexOf('')); 215 | 216 | done(); 217 | }); 218 | }); 219 | 220 | it('Should keep existing access rules unchanged in config.xml', function (done){ 221 | var testDir = path.join(workingDirectory, 'jsonPropertiesMissing'); 222 | var configXML = path.join(testDir, 'config.xml'); 223 | var ctx = initializeContext(testDir); 224 | 225 | updateConfiguration(ctx).then(function () { 226 | var content = fs.readFileSync(configXML).toString(); 227 | assert(content.indexOf('') > -1); 228 | assert(content.indexOf('') > -1); 229 | assert(content.indexOf('') > -1); 230 | 231 | done(); 232 | }); 233 | }); 234 | 235 | it('Should keep generic network access rules from config.xml', function (done){ 236 | var testDir = path.join(workingDirectory, 'fullAccessRules'); 237 | var configXML = path.join(testDir, 'config.xml'); 238 | var ctx = initializeContext(testDir); 239 | 240 | updateConfiguration(ctx).then(function () { 241 | var content = fs.readFileSync(configXML).toString(); 242 | assert(content.indexOf('') > 0); 243 | assert(content.indexOf('') > 0); 244 | 245 | done(); 246 | }); 247 | }); 248 | 249 | it('Should remove generic allow-intent rules from config.xml', function (done){ 250 | var testDir = path.join(workingDirectory, 'fullAccessRules'); 251 | var configXML = path.join(testDir, 'config.xml'); 252 | var ctx = initializeContext(testDir); 253 | 254 | updateConfiguration(ctx).then(function () { 255 | var content = fs.readFileSync(configXML).toString(); 256 | assert(content.indexOf('') === -1); 257 | assert(content.indexOf('') === -1); 258 | assert(content.indexOf('') === -1); 259 | 260 | done(); 261 | }); 262 | }); 263 | 264 | it('Should add allow-navigation rule for web site domain in config.xml if scope is missing', function (done){ 265 | var testDir = path.join(workingDirectory, 'normalFlow'); 266 | var configXML = path.join(testDir, 'config.xml'); 267 | var ctx = initializeContext(testDir); 268 | 269 | updateConfiguration(ctx).then(function () { 270 | var content = fs.readFileSync(configXML).toString(); 271 | 272 | assert(content.indexOf('') > 0); 273 | 274 | done(); 275 | }); 276 | }); 277 | 278 | it('Should add allow-navigation rule for scope in config.xml if scope is a relative URL', function (done){ 279 | var testDir = path.join(workingDirectory, 'xmlEmptyWidget'); 280 | var configXML = path.join(testDir, 'config.xml'); 281 | var ctx = initializeContext(testDir); 282 | 283 | updateConfiguration(ctx).then(function () { 284 | var content = fs.readFileSync(configXML).toString(); 285 | 286 | assert(content.indexOf('') > 0); 287 | 288 | done(); 289 | }); 290 | }); 291 | 292 | it('Should add allow-navigation rules for scope in config.xml if scope is a full URL', function (done){ 293 | var testDir = path.join(workingDirectory, 'fullUrlForScope'); 294 | var configXML = path.join(testDir, 'config.xml'); 295 | var ctx = initializeContext(testDir); 296 | 297 | updateConfiguration(ctx).then(function () { 298 | var content = fs.readFileSync(configXML).toString(); 299 | 300 | assert(content.indexOf('') > 0); 301 | 302 | done(); 303 | }); 304 | }); 305 | 306 | it('Should add allow-navigation rule for scope in config.xml if scope is a full URL with wildcard as subdomain', function (done){ 307 | var testDir = path.join(workingDirectory, 'wildcardSubdomainForScope'); 308 | var configXML = path.join(testDir, 'config.xml'); 309 | var ctx = initializeContext(testDir); 310 | 311 | updateConfiguration(ctx).then(function () { 312 | var content = fs.readFileSync(configXML).toString(); 313 | 314 | assert(content.indexOf('') > 0); 315 | 316 | done(); 317 | }); 318 | }); 319 | 320 | it('Should add allow-navigation rules from mjs_access_whitelist list', function (done){ 321 | var testDir = path.join(workingDirectory, 'xmlEmptyWidget'); 322 | var configXML = path.join(testDir, 'config.xml'); 323 | var ctx = initializeContext(testDir); 324 | 325 | updateConfiguration(ctx).then(function () { 326 | var content = fs.readFileSync(configXML).toString(); 327 | 328 | assert(content.indexOf('') > 0); 329 | assert(content.indexOf('') > 0); 330 | 331 | done(); 332 | }); 333 | }); 334 | 335 | afterEach(function () { 336 | tu.deleteRecursiveSync(workingDirectory); 337 | }); 338 | }); 339 | -------------------------------------------------------------------------------- /scripts/updateConfigurationAfterPrepare.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var createConfigParser = require('./createConfigParser'), 4 | fs = require('fs'), 5 | path = require('path'), 6 | config, 7 | windowsConfig, 8 | androidConfig, 9 | iosConfig, 10 | projectRoot, 11 | etree; 12 | 13 | var logger = { 14 | log: function () { 15 | if (process.env.NODE_ENV !== 'test') { 16 | console.log.apply(this, arguments) 17 | } 18 | } 19 | }; 20 | 21 | // Configure Cordova configuration parser 22 | function configureParser(context) { 23 | var cordova_util = context.requireCordovaModule('cordova-lib/src/cordova/util'); 24 | var ConfigParser; 25 | try { 26 | ConfigParser = context.requireCordovaModule('cordova-lib/node_modules/cordova-common').ConfigParser; 27 | } catch (err) { 28 | // Fallback to old location of config parser (old versions of cordova-lib) 29 | ConfigParser = context.requireCordovaModule('cordova-lib/src/configparser/ConfigParser'); 30 | } 31 | 32 | etree = context.requireCordovaModule('cordova-lib/node_modules/elementtree'); 33 | 34 | var xml = cordova_util.projectConfig(projectRoot); 35 | config = createConfigParser(xml, etree, ConfigParser); 36 | 37 | var windowsDir = path.join(projectRoot, 'platforms', 'windows'); 38 | if (fs.existsSync(windowsDir)) { 39 | var windowsXml = cordova_util.projectConfig(windowsDir); 40 | windowsConfig = createConfigParser(windowsXml, etree, ConfigParser); 41 | } 42 | 43 | var androidDir = path.join(projectRoot, 'platforms', 'android', 'res', 'xml'); 44 | if (fs.existsSync(androidDir)) { 45 | var androidXml = cordova_util.projectConfig(androidDir); 46 | androidConfig = createConfigParser(androidXml, etree, ConfigParser); 47 | } 48 | 49 | var iosProjectName = config.name(); 50 | if (iosProjectName) { 51 | var iosDir = path.join(projectRoot, 'platforms', 'ios', iosProjectName); 52 | if (fs.existsSync(iosDir)) { 53 | var iosXml = cordova_util.projectConfig(iosDir); 54 | iosConfig = createConfigParser(iosXml, etree, ConfigParser); 55 | } 56 | } 57 | } 58 | 59 | module.exports = function (context) { 60 | // create a parser for the Cordova configuration 61 | projectRoot = context.opts.projectRoot; 62 | configureParser(context); 63 | 64 | logger.log('Removing default images from Cordova configuration...'); 65 | 66 | // Remove default images from root configuration file 67 | config.removeElements('.//icon[@hap-default-image=\'yes\']'); 68 | config.removeElements('.//splash[@hap-default-image=\'yes\']'); 69 | 70 | // save the updated configuration 71 | config.write(); 72 | 73 | if (windowsConfig) { 74 | // Remove default images from windows configuration file 75 | windowsConfig.removeElements('.//icon[@hap-default-image=\'yes\']'); 76 | windowsConfig.removeElements('.//splash[@hap-default-image=\'yes\']'); 77 | 78 | windowsConfig.write(); 79 | } 80 | 81 | if (androidConfig) { 82 | // Remove default images from android configuration file 83 | androidConfig.removeElements('.//icon[@hap-default-image=\'yes\']'); 84 | androidConfig.removeElements('.//splash[@hap-default-image=\'yes\']'); 85 | 86 | androidConfig.write(); 87 | } 88 | 89 | if (iosConfig) { 90 | // Remove default images from ios configuration file 91 | iosConfig.removeElements('.//icon[@hap-default-image=\'yes\']'); 92 | iosConfig.removeElements('.//splash[@hap-default-image=\'yes\']'); 93 | 94 | iosConfig.write(); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /scripts/updateConfigurationBeforePrepare.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var fs = require('fs'), 4 | path = require('path'), 5 | url = require('url'), 6 | downloader = require('./downloader'), 7 | createConfigParser = require('./createConfigParser'), 8 | pendingTasks = [], 9 | Q, 10 | defaultIconsBaseDir, 11 | projectRoot, 12 | config, 13 | etree; 14 | 15 | var logger = { 16 | log: function () { 17 | if (process.env.NODE_ENV !== 'test') { 18 | console.log.apply(this, arguments) 19 | } 20 | }, 21 | warn: function() { 22 | if (process.env.NODE_ENV !== 'test') { 23 | console.warn.apply(this, arguments) 24 | } 25 | }, 26 | error: function() { 27 | if (process.env.NODE_ENV !== 'test') { 28 | console.error.apply(this, arguments) 29 | } 30 | } 31 | }; 32 | 33 | function ensurePathExists(pathName, callback) { 34 | fs.mkdir(pathName, function (err) { 35 | if (err) { 36 | if (err.code === 'ENOENT') { 37 | return ensurePathExists(path.dirname(pathName), function (err) { 38 | if (err && callback) { 39 | return callback && callback(err); 40 | } 41 | 42 | fs.mkdir(pathName, function (err) { 43 | if (err && err.code === 'EEXIST') { err = undefined; } 44 | callback && callback(err); 45 | }); 46 | }); 47 | } else if (err.code === 'EEXIST') { 48 | err = undefined; 49 | } 50 | } 51 | 52 | callback && callback(err); 53 | }); 54 | }; 55 | 56 | function downloadImage(imageUrl, imagesPath, imageSrc) { 57 | var deferral = new Q.defer(); 58 | pendingTasks.push(deferral.promise); 59 | 60 | ensurePathExists(imagesPath, function(err) { 61 | if (err && err.code !== 'EEXIST') { 62 | return logger.error("ERROR: Failed to create directory at: " + imagesPath + ' - ' + err.message); 63 | } 64 | 65 | downloader.downloadImage(imageUrl, imagesPath, function (err, data) { 66 | if (err) { 67 | var localPath = path.join(imagesPath, path.basename(imageSrc)); 68 | if (!fs.existsSync(localPath)) { 69 | logger.warn('WARNING: Failed to download icon file: ' + imageUrl + ' (' + err.message + ')'); 70 | } 71 | } else { 72 | if (data && data.statusCode !== 304) { 73 | logger.log('Downloaded icon file: ' + data.path); 74 | } 75 | } 76 | 77 | deferral.resolve(data); 78 | }); 79 | }); 80 | } 81 | 82 | // normalize image list and download images to project folder 83 | function processImageList(images, baseUrl) { 84 | var imageList = []; 85 | if (images && images instanceof Array) { 86 | images.forEach(function (image) { 87 | var imageUrl = url.resolve(baseUrl, image.src); 88 | image.src = url.parse(imageUrl).pathname; 89 | var sizes = image.sizes.toLowerCase().split(' '); 90 | sizes.forEach(function (imageSize) { 91 | var dimensions = imageSize.split('x'); 92 | var element = { 93 | "src": image.src, 94 | "width": dimensions[0], 95 | "height": dimensions[1], 96 | "density": image.density, 97 | "type": image.type 98 | }; 99 | 100 | imageList.push(element); 101 | }); 102 | 103 | var imagePath = path.dirname(path.join(projectRoot, image.src)); 104 | 105 | downloadImage(imageUrl, imagePath, image.src); 106 | }); 107 | } 108 | 109 | return imageList; 110 | } 111 | 112 | // Configure Cordova configuration parser 113 | function configureParser(context) { 114 | var cordova_util = context.requireCordovaModule('cordova-lib/src/cordova/util'); 115 | var ConfigParser; 116 | try { 117 | ConfigParser = context.requireCordovaModule('cordova-lib/node_modules/cordova-common').ConfigParser; 118 | } catch (err) { 119 | // Fallback to old location of config parser (old versions of cordova-lib) 120 | ConfigParser = context.requireCordovaModule('cordova-lib/src/configparser/ConfigParser'); 121 | } 122 | 123 | etree = context.requireCordovaModule('cordova-lib/node_modules/elementtree'); 124 | 125 | var xml = cordova_util.projectConfig(projectRoot); 126 | config = createConfigParser(xml, etree, ConfigParser); 127 | } 128 | 129 | function processAccessRules(manifest) { 130 | if (manifest && manifest.start_url) { 131 | 132 | // Remove previous rules added by the hook 133 | config.removeElements('.//allow-intent[@hap-rule=\'yes\']'); 134 | config.removeElements('.//allow-navigation[@hap-rule=\'yes\']'); 135 | config.removeElements('.//access[@hap-rule=\'yes\']'); 136 | 137 | // Remove "generic" rules to open external URLs outside the app 138 | config.removeElements('.//allow-intent[@href=\'http://*/*\']'); 139 | config.removeElements('.//allow-intent[@href=\'https://*/*\']'); 140 | config.removeElements('.//allow-intent[@href=\'*\']'); 141 | 142 | // determine base rule based on the start_url and the scope 143 | var baseUrlPattern = manifest.start_url; 144 | if (manifest.scope && manifest.scope.length) { 145 | var parsedScopeUrl = url.parse(manifest.scope); 146 | if (parsedScopeUrl.protocol) { 147 | baseUrlPattern = manifest.scope; 148 | } else { 149 | baseUrlPattern = url.resolve(baseUrlPattern, manifest.scope); 150 | } 151 | } 152 | 153 | // If there are no wildcards in the pattern, add '*' at the end 154 | if (baseUrlPattern.indexOf('*') === -1) { 155 | baseUrlPattern = url.resolve(baseUrlPattern, '*'); 156 | } 157 | 158 | // add base rule as a navigation rule 159 | var navigationBaseRule = new etree.SubElement(config.doc.getroot(), 'allow-navigation'); 160 | navigationBaseRule.set('hap-rule','yes'); 161 | navigationBaseRule.set('href', baseUrlPattern); 162 | 163 | var baseUrl = baseUrlPattern.substring(0, baseUrlPattern.length - 1);; 164 | 165 | // add additional navigation rules from mjs_access_whitelist 166 | // TODO: mjs_access_whitelist is deprecated. Should be removed in future versions 167 | if (manifest.mjs_access_whitelist && manifest.mjs_access_whitelist instanceof Array) { 168 | manifest.mjs_access_whitelist.forEach(function (item) { 169 | // To avoid duplicates, add the rule only if it does not have the base URL as a prefix 170 | if (item.url.indexOf(baseUrl) !== 0 ) { 171 | // add as a navigation rule 172 | var navigationEl = new etree.SubElement(config.doc.getroot(), 'allow-navigation'); 173 | navigationEl.set('hap-rule','yes'); 174 | navigationEl.set('href', item.url); 175 | } 176 | }); 177 | } 178 | 179 | // add additional navigation rules from mjs_extended_scope 180 | if (manifest.mjs_extended_scope && manifest.mjs_extended_scope instanceof Array) { 181 | manifest.mjs_extended_scope.forEach(function (item) { 182 | // To avoid duplicates, add the rule only if it does not have the base URL as a prefix 183 | if (item.indexOf(baseUrl) !== 0 ) { 184 | // add as a navigation rule 185 | var navigationEl = new etree.SubElement(config.doc.getroot(), 'allow-navigation'); 186 | navigationEl.set('hap-rule','yes'); 187 | navigationEl.set('href', item); 188 | } 189 | }); 190 | } 191 | } 192 | } 193 | 194 | function getFormatFromIcon(icon) { 195 | return icon.type || (icon.src && icon.src.split('.').pop()); 196 | } 197 | 198 | function isValidFormat(icon, validFormats) { 199 | if (!validFormats || validFormats.length === 0) { 200 | return true; 201 | } 202 | 203 | var iconFormat = getFormatFromIcon(icon); 204 | 205 | for (var i = 0; i < validFormats.length; i++) { 206 | if (validFormats[i].toLowerCase() === iconFormat) { 207 | return true; 208 | } 209 | } 210 | 211 | return false; 212 | } 213 | 214 | function processImagesBySize(platform, manifestImages, splashScreenSizes, iconSizes, validFormats) { 215 | // get platform section and create it if it does not exist 216 | var root = config.doc.find('platform[@name=\'' + platform + '\']'); 217 | if (!root) { 218 | root = etree.SubElement(config.doc.getroot(), 'platform'); 219 | root.set('name', platform); 220 | } 221 | 222 | var platformIcons = root.findall('icon'); 223 | var platformScreens = root.findall('splash'); 224 | manifestImages.forEach(function (element) { 225 | if (!isValidFormat(element, validFormats)) { 226 | return; 227 | } 228 | 229 | // Don't process the icon if the icon file does not exist 230 | if (!fs.existsSync(path.join(projectRoot, element.src))) { 231 | return; 232 | } 233 | 234 | var size = element.width + "x" + element.height; 235 | if (splashScreenSizes.indexOf(size) >= 0) { 236 | for (var screen, i = 0; i < platformScreens.length; i++) { 237 | if (element.width === platformScreens[i].get('width') && element.height === platformScreens[i].get('height')) { 238 | screen = platformScreens[i]; 239 | break; 240 | } 241 | } 242 | 243 | if (!screen) { 244 | screen = etree.SubElement(root, 'splash'); 245 | screen.set('width', element.width); 246 | screen.set('height', element.height); 247 | } 248 | 249 | screen.set('src', element.src); 250 | } 251 | else if (iconSizes.indexOf(size) >= 0) { 252 | for (var icon, i = 0; i < platformIcons.length; i++) { 253 | if (element.width === platformIcons[i].get('width') && element.height === platformIcons[i].get('height')) { 254 | icon = platformIcons[i]; 255 | break; 256 | } 257 | } 258 | 259 | if (!icon) { 260 | icon = etree.SubElement(root, 'icon'); 261 | icon.set('width', element.width); 262 | icon.set('height', element.height); 263 | } 264 | 265 | icon.set('src', element.src); 266 | } 267 | }); 268 | } 269 | 270 | function processImagesByDensity(platform, manifestImages, screenSizeToDensityMap, iconSizeToDensityMap, dppxToDensityMap, validFormats) { 271 | // get platform section and create it if it does not exist 272 | var root = config.doc.find('platform[@name=\'' + platform + '\']'); 273 | if (!root) { 274 | root = etree.SubElement(config.doc.getroot(), 'platform'); 275 | root.set('name', platform); 276 | } 277 | 278 | var platformIcons = root.findall('icon'); 279 | var platformScreens = root.findall('splash'); 280 | manifestImages.forEach(function (element) { 281 | if (!isValidFormat(element, validFormats)) { 282 | return; 283 | } 284 | 285 | // Don't process the icon if the icon file does not exist 286 | if (!fs.existsSync(path.join(projectRoot, element.src))) { 287 | return; 288 | } 289 | 290 | var size = element.width + "x" + element.height; 291 | var density = dppxToDensityMap[element.density]; 292 | var isScreen = screenSizeToDensityMap[size]; 293 | if (density && isScreen) { 294 | density = ((element.width > element.height) ? "land-" : "port-") + density; 295 | } 296 | 297 | var isIcon = iconSizeToDensityMap[element.width]; 298 | var screenDensity = density || isScreen; 299 | var iconDensity = density || isIcon; 300 | if (screenDensity && isScreen) { 301 | for (var screen, i = 0; i < platformScreens.length; i++) { 302 | if (screenDensity === platformScreens[i].get('density')) { 303 | screen = platformScreens[i]; 304 | break; 305 | } 306 | } 307 | 308 | if (!screen) { 309 | screen = etree.SubElement(root, 'splash'); 310 | screen.set('density', screenDensity); 311 | } 312 | 313 | screen.set('src', element.src); 314 | } 315 | else if (iconDensity && isIcon) { 316 | for (var icon, i = 0; i < platformIcons.length; i++) { 317 | if (iconDensity === platformIcons[i].get('density')) { 318 | icon = platformIcons[i]; 319 | break; 320 | } 321 | } 322 | 323 | if (!icon) { 324 | icon = etree.SubElement(root, 'icon'); 325 | icon.set('density', iconDensity); 326 | } 327 | 328 | icon.set('src', element.src); 329 | } 330 | }); 331 | } 332 | 333 | function processDefaultIconsByDensity(platform, screenDensities, iconDensities) { 334 | // get platform section and create it if it does not exist 335 | var root = config.doc.find('platform[@name=\'' + platform + '\']'); 336 | if (!root) { 337 | root = etree.SubElement(config.doc.getroot(), 'platform'); 338 | root.set('name', platform); 339 | } 340 | 341 | var platformIcons = root.findall('icon'); 342 | var platformScreens = root.findall('splash'); 343 | 344 | iconDensities.forEach(function (iconDensity) { 345 | for (var icon, i = 0; i < platformIcons.length; i++) { 346 | if (iconDensity === platformIcons[i].get('density')) { 347 | icon = platformIcons[i]; 348 | break; 349 | } 350 | } 351 | 352 | if (!icon) { 353 | var iconSrc = defaultIconsBaseDir + '/' + platform + '/' + iconDensity + '.png'; 354 | 355 | icon = etree.SubElement(root, 'icon'); 356 | icon.set('hap-default-image', 'yes'); 357 | icon.set('density', iconDensity); 358 | icon.set('src', iconSrc); 359 | } 360 | }); 361 | 362 | screenDensities.forEach(function (screenDensity) { 363 | for (var screen, i = 0; i < platformScreens.length; i++) { 364 | if (screenDensity === platformScreens[i].get('density')) { 365 | screen = platformScreens[i]; 366 | break; 367 | } 368 | } 369 | 370 | if (!screen) { 371 | var screenSrc = defaultIconsBaseDir + '/' + platform + '/' + screenDensity + '.png'; 372 | 373 | screen = etree.SubElement(root, 'splash'); 374 | screen.set('hap-default-image', 'yes'); 375 | screen.set('density', screenDensity); 376 | screen.set('src', screenSrc); 377 | } 378 | }); 379 | } 380 | 381 | function processDefaultIconsBySize(platform, screenSizes, iconSizes) { 382 | // get platform section and create it if it does not exist 383 | var root = config.doc.find('platform[@name=\'' + platform + '\']'); 384 | if (!root) { 385 | root = etree.SubElement(config.doc.getroot(), 'platform'); 386 | root.set('name', platform); 387 | } 388 | 389 | var platformIcons = root.findall('icon'); 390 | var platformScreens = root.findall('splash'); 391 | 392 | iconSizes.forEach(function (iconSize) { 393 | var dimensions = iconSize.split('x'); 394 | var iconWidth = dimensions[0]; 395 | var iconHeight = dimensions[1]; 396 | 397 | for (var icon, i = 0; i < platformIcons.length; i++) { 398 | if (iconWidth === platformIcons[i].get('width') && iconHeight === platformIcons[i].get('height')) { 399 | icon = platformIcons[i]; 400 | break; 401 | } 402 | } 403 | 404 | if (!icon) { 405 | var iconSrc = defaultIconsBaseDir + '/' + platform + '/' + iconSize + '.png'; 406 | 407 | icon = etree.SubElement(root, 'icon'); 408 | icon.set('hap-default-image', 'yes'); 409 | icon.set('width', iconWidth); 410 | icon.set('height', iconHeight); 411 | icon.set('src', iconSrc); 412 | } 413 | }); 414 | 415 | screenSizes.forEach(function (screenSize) { 416 | var dimensions = screenSize.split('x'); 417 | var screenWidth = dimensions[0]; 418 | var screenHeight = dimensions[1]; 419 | 420 | for (var screen, i = 0; i < platformScreens.length; i++) { 421 | if (screenWidth === platformScreens[i].get('width') && screenHeight === platformScreens[i].get('height')) { 422 | screen = platformScreens[i]; 423 | break; 424 | } 425 | } 426 | 427 | if (!screen) { 428 | var screenSrc = defaultIconsBaseDir + '/' + platform + '/' + screenSize + '.png'; 429 | 430 | screen = etree.SubElement(root, 'splash'); 431 | screen.set('hap-default-image', 'yes'); 432 | screen.set('width', screenWidth); 433 | screen.set('height', screenHeight); 434 | screen.set('src', screenSrc); 435 | } 436 | }); 437 | } 438 | 439 | function processiOSIcons(manifestIcons, manifestSplashScreens) { 440 | var iconSizes = [ 441 | "40x40", 442 | "80x80", 443 | "50x50", 444 | "100x100", 445 | "57x57", 446 | "114x114", 447 | "60x60", 448 | "120x120", 449 | "180x180", 450 | "72x72", 451 | "144x144", 452 | "76x76", 453 | "152x152", 454 | "29x29", 455 | "58x58" 456 | ]; 457 | 458 | var splashScreenSizes = [ 459 | "1024x768", 460 | "2048x1536", 461 | "768x1024", 462 | "1536x2048", 463 | "640x1136", 464 | "2208x1242", 465 | "320x480", 466 | "640x960", 467 | "750x1334", 468 | "1242x2208" 469 | ]; 470 | 471 | processImagesBySize('ios', manifestIcons, splashScreenSizes, iconSizes); 472 | processImagesBySize('ios', manifestSplashScreens, splashScreenSizes, []); 473 | processDefaultIconsBySize('ios', splashScreenSizes, iconSizes); 474 | } 475 | 476 | function processAndroidIcons(manifestIcons, manifestSplashScreens) { 477 | var iconSizeToDensityMap = { 478 | 36: 'ldpi', 479 | 48: 'mdpi', 480 | 72: 'hdpi', 481 | 96: 'xhdpi', 482 | 144: 'xxhdpi', 483 | 192: 'xxxhdpi' 484 | }; 485 | 486 | var dppxToDensityMap = { 487 | 0.75: 'ldpi', 488 | 1: 'mdpi', 489 | 1.5: 'hdpi', 490 | 2: 'xhdpi', 491 | 3: 'xxhdpi', 492 | 4: 'xxxhdpi' 493 | }; 494 | 495 | var screenSizeToDensityMap = { 496 | "800x480": "land-hdpi", 497 | "320x200": "land-ldpi", 498 | "480x320": "land-mdpi", 499 | "1280x720":"land-xhdpi", 500 | "480x800": "port-hdpi", 501 | "200x320": "port-ldpi", 502 | "320x480": "port-mdpi", 503 | "720x1280":"port-xhdpi" 504 | }; 505 | 506 | var iconDensities = []; 507 | for (var size in iconSizeToDensityMap) { 508 | if (iconSizeToDensityMap.hasOwnProperty(size)) { 509 | iconDensities.push(iconSizeToDensityMap[size]); 510 | } 511 | } 512 | 513 | var screenDensities = []; 514 | for (var size in screenSizeToDensityMap) { 515 | if (screenSizeToDensityMap.hasOwnProperty(size)) { 516 | screenDensities.push(screenSizeToDensityMap[size]); 517 | } 518 | } 519 | 520 | var validFormats = [ 521 | 'png', 522 | 'image/png' 523 | ]; 524 | 525 | processImagesByDensity('android', manifestIcons, screenSizeToDensityMap, iconSizeToDensityMap, dppxToDensityMap, validFormats); 526 | processImagesByDensity('android', manifestSplashScreens, screenSizeToDensityMap, [], dppxToDensityMap, validFormats); 527 | processDefaultIconsByDensity('android', screenDensities, iconDensities); 528 | } 529 | 530 | function processWindowsIcons(manifestIcons, manifestSplashScreens) { 531 | var iconSizes = [ 532 | "30x30", 533 | "44x44", 534 | "106x106", 535 | "70x70", 536 | "71x71", 537 | "170x170", 538 | "150x150", 539 | "360x360", 540 | "310x310", 541 | "50x50", 542 | "120x120", 543 | "310x150", 544 | "744x360" 545 | ]; 546 | 547 | var splashScreenSizes = [ 548 | "620x300", 549 | "1152x1920" 550 | ]; 551 | 552 | processImagesBySize('windows', manifestIcons, splashScreenSizes, iconSizes); 553 | processImagesBySize('windows', manifestSplashScreens, splashScreenSizes, []); 554 | processDefaultIconsBySize('windows', splashScreenSizes, iconSizes); 555 | }; 556 | 557 | function processWindowsPhoneIcons(manifestIcons, manifestSplashScreens) { 558 | var iconSizes = [ 559 | "62x62", 560 | "173x173" 561 | ]; 562 | 563 | var splashScreenSizes = [ 564 | "480x800" 565 | ]; 566 | 567 | processImagesBySize('wp8', manifestIcons, splashScreenSizes, iconSizes); 568 | processImagesBySize('wp8', manifestSplashScreens, splashScreenSizes, []); 569 | processDefaultIconsBySize('wp8', splashScreenSizes, iconSizes); 570 | }; 571 | 572 | module.exports = function (context) { 573 | logger.log('Updating Cordova configuration from W3C manifest...'); 574 | 575 | Q = context.requireCordovaModule('q'); 576 | 577 | // Get base path for default icons 578 | defaultIconsBaseDir = 'plugins/' + context.opts.plugin.id + '/assets/defaultImages'; 579 | 580 | // create a parser for the Cordova configuration 581 | projectRoot = context.opts.projectRoot; 582 | configureParser(context); 583 | 584 | // read W3C manifest 585 | var task = Q.defer(); 586 | 587 | var manifestPath = path.join(projectRoot, 'manifest.json'); 588 | fs.readFile(manifestPath, function (err, data) { 589 | if (err) { 590 | logger.warn('Failed to read manifest at \'' + manifestPath + '\'. Proceeding to point config.xml to sample url of https://www.npmjs.com/package/cordova-plugin-hostedwebapp.'); 591 | data = JSON.stringify({ 'start_url': 'https://www.npmjs.com/package/cordova-plugin-hostedwebapp', 'short_name' : 'PlaceholderSite'}); 592 | } 593 | 594 | var manifestJson = data.toString().replace(/^\uFEFF/, ''); 595 | 596 | var appManifestPath = path.join(projectRoot, 'www', 'manifest.json'); 597 | fs.writeFile(appManifestPath, manifestJson, function (err) { 598 | if (err) { 599 | logger.error('Failed to copy manifest to \'www\' folder.'); 600 | return task.reject(err); 601 | } 602 | 603 | var manifest = JSON.parse(manifestJson); 604 | 605 | // The start_url member is required and must be a full URL. 606 | // Even though a relative URL is a valid according to the W3C spec, a full URL 607 | // is needed because the plugin cannot determine the manifest's origin. 608 | var start_url; 609 | if (manifest.start_url) { 610 | start_url = url.parse(manifest.start_url); 611 | } 612 | 613 | if (!(start_url && start_url.hostname && start_url.protocol)) { 614 | logger.error('Invalid or incomplete W3C manifest.'); 615 | var err = new Error('The start_url member in the manifest is required and must be a full URL.'); 616 | return task.reject(err); 617 | } 618 | 619 | // update name, start_url, orientation, and fullscreen from manifest 620 | if (manifest.short_name) { 621 | config.setName(manifest.short_name.replace(/\//g,'').replace(/\s/g,'')); 622 | } else if (manifest.name) { 623 | config.setName(manifest.name.replace(/\//g,'').replace(/\s/g,'')); 624 | } 625 | 626 | config.setAttribute('content', 'src', manifest.start_url); 627 | config.setPreference('Orientation', (function(orientation){ 628 | // map W3C manifest orientation options to Cordova orientation options 629 | switch (orientation){ 630 | case "any": 631 | case "natural": 632 | return "default"; 633 | 634 | case "landscape": 635 | case "landscape-primary": 636 | case "landscape-secondary": 637 | return "landscape"; 638 | 639 | case "portrait": 640 | case "portrait-primary": 641 | case "portrait-secondary": 642 | return "portrait"; 643 | } 644 | 645 | })(manifest.orientation)); 646 | 647 | if (manifest.display) { 648 | config.setPreference('Fullscreen', manifest.display == 'fullscreen' ? 'true' : 'false'); 649 | } 650 | 651 | // configure access rules 652 | processAccessRules(manifest); 653 | 654 | // Obtain and download the icons and splash screens specified in the manifest. 655 | // Currently, splash screens specified in the splash_screens section of the manifest 656 | // take precedence over similarly sized splash screens in the icons section. 657 | var manifestIcons = processImageList(manifest.icons, manifest.start_url); 658 | var manifestSplashScreens = processImageList(manifest.splash_screens, manifest.start_url); 659 | 660 | Q.allSettled(pendingTasks).then(function () { 661 | 662 | // Configure the icons once all icon files are downloaded 663 | processiOSIcons(manifestIcons, manifestSplashScreens); 664 | processAndroidIcons(manifestIcons, manifestSplashScreens); 665 | processWindowsIcons(manifestIcons, manifestSplashScreens); 666 | processWindowsPhoneIcons(manifestIcons, manifestSplashScreens); 667 | 668 | // save the updated configuration 669 | config.write(); 670 | 671 | task.resolve(); 672 | }); 673 | }); 674 | }); 675 | 676 | return task.promise; 677 | } 678 | -------------------------------------------------------------------------------- /src/android/HostedWebApp.java: -------------------------------------------------------------------------------- 1 | package com.manifoldjs.hostedwebapp; 2 | 3 | import android.content.Intent; 4 | import android.net.Uri; 5 | import android.content.res.AssetManager; 6 | import android.util.Log; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.webkit.ValueCallback; 10 | import android.webkit.WebView; 11 | import android.webkit.WebViewClient; 12 | import android.widget.LinearLayout; 13 | 14 | import org.apache.cordova.CallbackContext; 15 | import org.apache.cordova.CordovaActivity; 16 | import org.apache.cordova.CordovaPlugin; 17 | 18 | import org.apache.cordova.PluginResult; 19 | import org.apache.cordova.Whitelist; 20 | import org.json.JSONArray; 21 | import org.json.JSONException; 22 | import org.json.JSONObject; 23 | 24 | import java.io.IOException; 25 | import java.io.InputStream; 26 | import java.lang.reflect.Method; 27 | import java.net.HttpURLConnection; 28 | import java.net.URL; 29 | import java.util.ArrayList; 30 | import java.util.Arrays; 31 | import java.util.List; 32 | 33 | /** 34 | * This class manipulates the Web App W3C manifest. 35 | */ 36 | public class HostedWebApp extends CordovaPlugin { 37 | private static final String LOG_TAG = "HostedWebApp"; 38 | private static final String DEFAULT_MANIFEST_FILE = "manifest.json"; 39 | private static final String OFFLINE_PAGE = "offline.html"; 40 | private static final String OFFLINE_PAGE_TEMPLATE = "
%s
"; 41 | 42 | private boolean loadingManifest; 43 | private JSONObject manifestObject; 44 | 45 | private CordovaActivity activity; 46 | private CordovaPlugin whiteListPlugin; 47 | 48 | private LinearLayout rootLayout; 49 | private WebView offlineWebView; 50 | private boolean offlineOverlayEnabled = true; 51 | 52 | private boolean isConnectionError = false; 53 | 54 | @Override 55 | public void pluginInitialize() { 56 | final HostedWebApp me = HostedWebApp.this; 57 | this.activity = (CordovaActivity)this.cordova.getActivity(); 58 | 59 | // Load default manifest file. 60 | this.loadingManifest = true; 61 | if (this.assetExists(HostedWebApp.DEFAULT_MANIFEST_FILE)) { 62 | try { 63 | this.manifestObject = this.loadLocalManifest(HostedWebApp.DEFAULT_MANIFEST_FILE); 64 | this.onManifestLoaded(); 65 | } catch (JSONException e) { 66 | e.printStackTrace(); 67 | } 68 | } 69 | 70 | this.loadingManifest = false; 71 | 72 | if (!this.manifestObject.optBoolean("mjs_offline_feature", true)) { 73 | this.offlineOverlayEnabled = false; 74 | // Do not initialize offline overlay 75 | return; 76 | } 77 | // Initialize offline overlay 78 | this.activity.runOnUiThread(new Runnable() { 79 | @Override 80 | public void run() { 81 | if (me.rootLayout == null) { 82 | me.rootLayout = me.createOfflineRootLayout(); 83 | me.activity.addContentView(me.rootLayout, me.rootLayout.getLayoutParams()); 84 | } 85 | 86 | if (me.offlineWebView == null) { 87 | me.offlineWebView = me.createOfflineWebView(); 88 | me.rootLayout.addView(me.offlineWebView); 89 | } 90 | 91 | if (me.assetExists(HostedWebApp.OFFLINE_PAGE)) { 92 | me.offlineWebView.loadUrl("file:///android_asset/www/" + HostedWebApp.OFFLINE_PAGE); 93 | } else { 94 | me.offlineWebView.loadData( 95 | String.format(HostedWebApp.OFFLINE_PAGE_TEMPLATE, "It looks like you are offline. Please reconnect to use this application."), 96 | "text/html", 97 | null); 98 | } 99 | 100 | me.offlineOverlayEnabled = true; 101 | } 102 | }); 103 | } 104 | 105 | @Override 106 | public boolean execute(String action, JSONArray args, final CallbackContext callbackContext) throws JSONException { 107 | final HostedWebApp me = HostedWebApp.this; 108 | if (action.equals("getManifest")) { 109 | if (this.manifestObject != null) { 110 | callbackContext.success(manifestObject.toString()); 111 | } else { 112 | callbackContext.error("Manifest not loaded, load a manifest using loadManifest."); 113 | } 114 | 115 | return true; 116 | } 117 | 118 | if (action.equals("loadManifest")) { 119 | if (this.loadingManifest) { 120 | callbackContext.error("Already loading a manifest"); 121 | } else if (args.length() == 0) { 122 | callbackContext.error("Manifest file name required"); 123 | } else { 124 | final String configFilename = args.getString(0); 125 | 126 | this.loadingManifest = true; 127 | this.cordova.getThreadPool().execute(new Runnable() { 128 | @Override 129 | public void run() { 130 | if (me.assetExists(configFilename)) { 131 | try { 132 | me.manifestObject = me.loadLocalManifest(configFilename); 133 | me.onManifestLoaded(); 134 | callbackContext.success(me.manifestObject); 135 | } catch (JSONException e) { 136 | callbackContext.error(e.getMessage()); 137 | } 138 | } else { 139 | callbackContext.error("Manifest file not found in folder assets/www"); 140 | } 141 | 142 | me.loadingManifest = false; 143 | } 144 | }); 145 | 146 | PluginResult pluginResult = new PluginResult(PluginResult.Status.NO_RESULT); 147 | pluginResult.setKeepCallback(true); 148 | callbackContext.sendPluginResult(pluginResult); 149 | } 150 | 151 | return true; 152 | } 153 | 154 | if (action.equals("enableOfflinePage")) { 155 | this.offlineOverlayEnabled = true; 156 | return true; 157 | } 158 | 159 | if (action.equals("disableOfflinePage")) { 160 | this.offlineOverlayEnabled = false; 161 | return true; 162 | } 163 | 164 | if (action.equals("injectPluginScript")) { 165 | final List scripts = new ArrayList(); 166 | scripts.add(args.getString(0)); 167 | 168 | cordova.getActivity().runOnUiThread(new Runnable() { 169 | @Override 170 | public void run() { 171 | injectScripts(scripts, new ValueCallback() { 172 | @Override 173 | public void onReceiveValue(String s) { 174 | callbackContext.success(1); 175 | } 176 | }); 177 | } 178 | }); 179 | 180 | return true; 181 | } 182 | 183 | return false; 184 | } 185 | 186 | @Override 187 | public Object onMessage(String id, Object data) { 188 | if (id.equals("networkconnection") && data != null) { 189 | this.handleNetworkConnectionChange(data.toString()); 190 | } else if (id.equals("onPageStarted")) { 191 | this.isConnectionError = false; 192 | } else if (id.equals("onReceivedError")) { 193 | if (data instanceof JSONObject) { 194 | JSONObject errorData = (JSONObject) data; 195 | try { 196 | int errorCode = errorData.getInt("errorCode"); 197 | if (404 == errorCode 198 | || WebViewClient.ERROR_HOST_LOOKUP == errorCode 199 | || WebViewClient.ERROR_CONNECT == errorCode 200 | || WebViewClient.ERROR_TIMEOUT == errorCode) { 201 | this.isConnectionError = true; 202 | this.showOfflineOverlay(); 203 | } 204 | } catch (JSONException e) { 205 | e.printStackTrace(); 206 | } 207 | } 208 | } 209 | else if (id.equals("onPageFinished")) { 210 | if (!this.isConnectionError) { 211 | this.hideOfflineOverlay(); 212 | } 213 | 214 | if (data != null) { 215 | String url = data.toString(); 216 | Log.v(LOG_TAG, String.format("Finished loading URL '%s'", url)); 217 | 218 | this.injectCordovaScripts(url); 219 | } 220 | } 221 | 222 | return null; 223 | } 224 | 225 | @Override 226 | public Boolean shouldAllowRequest(String url) { 227 | CordovaPlugin whiteListPlugin = this.getWhitelistPlugin(); 228 | 229 | if (whiteListPlugin != null && Boolean.TRUE != whiteListPlugin.shouldAllowRequest(url)) { 230 | Log.w(LOG_TAG, String.format("Whitelist rejection: url='%s'", url)); 231 | } 232 | 233 | // do not alter default behavior. 234 | return super.shouldAllowRequest(url); 235 | } 236 | 237 | @Override 238 | public boolean onOverrideUrlLoading(String url) { 239 | CordovaPlugin whiteListPlugin = this.getWhitelistPlugin(); 240 | 241 | if (whiteListPlugin != null && Boolean.TRUE != whiteListPlugin.shouldAllowNavigation(url)) { 242 | // If the URL is not in the list URLs to allow navigation, open the URL in the external browser 243 | // (code extracted from CordovaLib/src/org/apache/cordova/CordovaWebViewImpl.java) 244 | Log.w(LOG_TAG, String.format("Whitelist rejection: url='%s'", url)); 245 | 246 | try { 247 | Intent intent = new Intent(Intent.ACTION_VIEW); 248 | intent.addCategory(Intent.CATEGORY_BROWSABLE); 249 | Uri uri = Uri.parse(url); 250 | // Omitting the MIME type for file: URLs causes "No Activity found to handle Intent". 251 | // Adding the MIME type to http: URLs causes them to not be handled by the downloader. 252 | if ("file".equals(uri.getScheme())) { 253 | intent.setDataAndType(uri, this.webView.getResourceApi().getMimeType(uri)); 254 | } else { 255 | intent.setData(uri); 256 | } 257 | this.activity.startActivity(intent); 258 | } catch (android.content.ActivityNotFoundException e) { 259 | e.printStackTrace(); 260 | } 261 | 262 | return true; 263 | } else { 264 | return false; 265 | } 266 | } 267 | 268 | public JSONObject getManifest() { 269 | return this.manifestObject; 270 | } 271 | 272 | private void injectCordovaScripts(String pageUrl) { 273 | 274 | // Inject cordova scripts 275 | JSONArray apiAccessRules = this.manifestObject.optJSONArray("mjs_api_access"); 276 | if (apiAccessRules != null) { 277 | boolean allowApiAccess = false; 278 | for (int i = 0; i < apiAccessRules.length(); i++) { 279 | JSONObject apiRule = apiAccessRules.optJSONObject(i); 280 | if (apiRule != null) { 281 | // ensure rule applies to current platform and current page 282 | if (this.isMatchingRuleForPage(pageUrl, apiRule, true)) { 283 | String access = apiRule.optString("access", "cordova").trim(); 284 | if (access.equalsIgnoreCase("cordova")) { 285 | allowApiAccess = true; 286 | } else if (access.equalsIgnoreCase("none")) { 287 | allowApiAccess = false; 288 | break; 289 | } else { 290 | Log.v(LOG_TAG, String.format("Unsupported API access type '%s' found in mjs_api_access rule.", access)); 291 | } 292 | } 293 | } 294 | } 295 | 296 | if (allowApiAccess) { 297 | String pluginMode = "client"; 298 | String cordovaBaseUrl = "/"; 299 | 300 | JSONObject cordovaSettings = this.manifestObject.optJSONObject("mjs_cordova"); 301 | if (cordovaSettings != null) { 302 | pluginMode = cordovaSettings.optString("plugin_mode", "client").trim(); 303 | cordovaBaseUrl = cordovaSettings.optString("base_url", "").trim(); 304 | if (!cordovaBaseUrl.endsWith("/")) { 305 | cordovaBaseUrl += "/"; 306 | } 307 | } 308 | 309 | this.webView.getEngine().loadUrl("javascript: window.hostedWebApp = { 'platform': 'android', 'pluginMode': '" + pluginMode + "', 'cordovaBaseUrl': '" + cordovaBaseUrl + "'};", false); 310 | 311 | List scriptList = new ArrayList(); 312 | if (pluginMode.equals("client")) { 313 | scriptList.add("cordova.js"); 314 | } 315 | 316 | scriptList.add("hostedapp-bridge.js"); 317 | injectScripts(scriptList, null); 318 | } 319 | } 320 | 321 | // Inject custom scripts 322 | JSONArray customScripts = this.manifestObject.optJSONArray("mjs_import_scripts"); 323 | if (customScripts != null && customScripts.length() > 0) { 324 | for (int i = 0; i < customScripts.length(); i++) { 325 | JSONObject item = customScripts.optJSONObject(i); 326 | if (item != null) { 327 | String source = item.optString("src", "").trim(); 328 | if (!source.isEmpty()) { 329 | // ensure script applies to current page 330 | if (this.isMatchingRuleForPage(pageUrl, item, false)) { 331 | injectScripts(Arrays.asList(new String[]{source}), null); 332 | } 333 | } 334 | } 335 | } 336 | } 337 | } 338 | 339 | private boolean isMatchingRuleForPage(String pageUrl, JSONObject item, boolean checkPlatform) { 340 | // ensure item applies to current platform 341 | if (checkPlatform) { 342 | boolean isPlatformMatch = true; 343 | String platform = item.optString("platform", "").trim(); 344 | if (!platform.isEmpty()) { 345 | isPlatformMatch = false; 346 | String[] platforms = platform.split(","); 347 | for (String p : platforms) { 348 | if (p.trim().equalsIgnoreCase("android")) { 349 | isPlatformMatch = true; 350 | break; 351 | } 352 | } 353 | } 354 | 355 | if (!isPlatformMatch) { 356 | return false; 357 | } 358 | } 359 | 360 | // ensure item applies to current page 361 | boolean isURLMatch = true; 362 | JSONArray match = item.optJSONArray("match"); 363 | if (match == null) { 364 | match = new JSONArray(); 365 | String matchString = item.optString("match", "").trim(); 366 | if (!matchString.isEmpty()) { 367 | match.put(matchString); 368 | } 369 | } 370 | 371 | if (match.length() > 0) { 372 | Whitelist whitelist = new Whitelist(); 373 | for (int j = 0; j < match.length(); j++) { 374 | whitelist.addWhiteListEntry(match.optString(j).trim(), false); 375 | } 376 | 377 | isURLMatch = whitelist.isUrlWhiteListed(pageUrl); 378 | } 379 | 380 | return isURLMatch; 381 | } 382 | 383 | private void onManifestLoaded() { 384 | this.webView.postMessage("hostedWebApp_manifestLoaded", this.manifestObject); 385 | } 386 | 387 | private CordovaPlugin getWhitelistPlugin() { 388 | if (this.whiteListPlugin == null) { 389 | this.whiteListPlugin = this.webView.getPluginManager().getPlugin("Whitelist"); 390 | } 391 | 392 | return whiteListPlugin; 393 | } 394 | 395 | private boolean assetExists(String asset) { 396 | final AssetManager assetManager = this.activity.getResources().getAssets(); 397 | try { 398 | return Arrays.asList(assetManager.list("www")).contains(asset); 399 | } catch (IOException e) { 400 | e.printStackTrace(); 401 | } 402 | 403 | return false; 404 | } 405 | 406 | private WebView createOfflineWebView() { 407 | WebView webView = new WebView(activity); 408 | webView.getSettings().setJavaScriptEnabled(true); 409 | 410 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) { 411 | webView.setLayerType(View.LAYER_TYPE_SOFTWARE, null); 412 | } 413 | 414 | webView.setLayoutParams(new LinearLayout.LayoutParams( 415 | ViewGroup.LayoutParams.MATCH_PARENT, 416 | ViewGroup.LayoutParams.MATCH_PARENT, 417 | 1.0F)); 418 | return webView; 419 | } 420 | 421 | private LinearLayout createOfflineRootLayout() { 422 | LinearLayout root = new LinearLayout(activity.getBaseContext()); 423 | root.setOrientation(LinearLayout.VERTICAL); 424 | root.setVisibility(View.INVISIBLE); 425 | root.setLayoutParams(new LinearLayout.LayoutParams( 426 | ViewGroup.LayoutParams.MATCH_PARENT, 427 | ViewGroup.LayoutParams.MATCH_PARENT, 428 | 0.0F)); 429 | return root; 430 | } 431 | 432 | private void handleNetworkConnectionChange(String info) { 433 | final HostedWebApp me = HostedWebApp.this; 434 | if (info.equals("none")) { 435 | this.showOfflineOverlay(); 436 | } else { 437 | if (this.isConnectionError) { 438 | 439 | this.activity.runOnUiThread(new Runnable() { 440 | @Override 441 | public void run() { 442 | String currentUrl = me.webView.getUrl(); 443 | me.webView.loadUrlIntoView(currentUrl, false); 444 | } 445 | }); 446 | } else { 447 | this.hideOfflineOverlay(); 448 | } 449 | } 450 | } 451 | 452 | private void showOfflineOverlay() { 453 | final HostedWebApp me = HostedWebApp.this; 454 | if (this.offlineOverlayEnabled) { 455 | this.activity.runOnUiThread(new Runnable() { 456 | @Override 457 | public void run() { 458 | if (me.rootLayout != null) { 459 | me.rootLayout.setVisibility(View.VISIBLE); 460 | } 461 | } 462 | }); 463 | } 464 | } 465 | 466 | private void hideOfflineOverlay() { 467 | final HostedWebApp me = HostedWebApp.this; 468 | this.activity.runOnUiThread(new Runnable() { 469 | @Override 470 | public void run() { 471 | if (me.rootLayout != null) { 472 | me.rootLayout.setVisibility(View.INVISIBLE); 473 | } 474 | } 475 | }); 476 | } 477 | 478 | private JSONObject loadLocalManifest(String manifestFile) throws JSONException { 479 | try { 480 | InputStream inputStream = this.activity.getResources().getAssets().open("www/" + manifestFile); 481 | int size = inputStream.available(); 482 | byte[] bytes = new byte[size]; 483 | inputStream.read(bytes); 484 | inputStream.close(); 485 | String jsonString = new String(bytes, "UTF-8"); 486 | return new JSONObject(jsonString); 487 | } catch (IOException e) { 488 | e.printStackTrace(); 489 | } 490 | 491 | return null; 492 | } 493 | 494 | private void injectScripts(final List files, final ValueCallback resultCallback) { 495 | final HostedWebApp me = this; 496 | 497 | this.cordova.getThreadPool().execute(new Runnable() { 498 | @Override 499 | public void run() { 500 | String script = ""; 501 | for (int i = 0; i < files.size(); i++) { 502 | String fileName = files.get(i); 503 | String content = ""; 504 | Log.w(LOG_TAG, String.format("Injecting script: '%s'", fileName)); 505 | 506 | try { 507 | Uri uri = Uri.parse(fileName); 508 | if (uri.isRelative()) { 509 | // Load script file from assets 510 | try { 511 | InputStream inputStream = me.activity.getResources().getAssets().open("www/" + fileName); 512 | content = me.ReadStreamContent(inputStream); 513 | 514 | } catch (IOException e) { 515 | Log.v(LOG_TAG, String.format("ERROR: failed to load script file: '%s'", fileName)); 516 | e.printStackTrace(); 517 | } 518 | } else { 519 | // load script file from URL 520 | URL url = new URL(fileName); 521 | try { 522 | HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); 523 | try { 524 | InputStream inputStream = urlConnection.getInputStream(); 525 | content = me.ReadStreamContent(inputStream); 526 | } finally { 527 | urlConnection.disconnect(); 528 | } 529 | } catch (IOException e) { 530 | Log.v(LOG_TAG, String.format("ERROR: failed to load script file from URL: '%s'", fileName)); 531 | e.printStackTrace(); 532 | } 533 | } 534 | } catch (Exception e) { 535 | Log.v(LOG_TAG, String.format("ERROR: Invalid path format of script file: '%s'", fileName)); 536 | e.printStackTrace(); 537 | } 538 | 539 | if (!content.isEmpty()) { 540 | script += "\r\n//# sourceURL=" + fileName + "\r\n" + content; 541 | } 542 | } 543 | 544 | final String scriptToInject = script; 545 | me.activity.runOnUiThread(new Runnable() { 546 | @Override 547 | public void run() { 548 | View webView = me.webView.getEngine().getView(); 549 | 550 | try { 551 | Method evaluateJavaScriptMethod = webView.getClass().getMethod("evaluateJavascript", new Class[]{ String.class, (Class>)(Class)ValueCallback.class }); 552 | evaluateJavaScriptMethod.invoke(webView, scriptToInject, resultCallback); 553 | } catch (Exception e) { 554 | Log.v(LOG_TAG, String.format("WARNING: Webview does not support 'evaluateJavascript' method. Webview type: '%s'", webView.getClass().getName())); 555 | me.webView.getEngine().loadUrl("javascript:" + scriptToInject, false); 556 | 557 | if (resultCallback != null) { 558 | resultCallback.onReceiveValue(null); 559 | } 560 | } 561 | } 562 | }); 563 | } 564 | }); 565 | } 566 | 567 | private String ReadStreamContent(InputStream inputStream) throws IOException { 568 | int size = inputStream.available(); 569 | byte[] bytes = new byte[size]; 570 | inputStream.read(bytes); 571 | inputStream.close(); 572 | String content = new String(bytes, "UTF-8"); 573 | 574 | return content; 575 | } 576 | } 577 | -------------------------------------------------------------------------------- /src/ios/CDVHostedWebApp.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #define kManifestLoadedNotification @"kManifestLoadedNotification" 4 | 5 | #define kCDVHostedWebAppWebViewDidStartLoad @"CDVHostedWebAppWebViewDidStartLoad" 6 | #define kCDVHostedWebAppWebViewShouldStartLoadWithRequest @"CDVHostedWebAppWebViewShouldStartLoadWithRequest" 7 | #define kCDVHostedWebAppWebViewDidFinishLoad @"CDVHostedWebAppWebViewDidFinishLoad" 8 | #define kCDVHostedWebAppWebViewDidFailLoadWithError @"CDVHostedWebAppWebViewDidFailLoadWithError" 9 | 10 | @interface CVDWebViewNotificationDelegate : NSObject 11 | @property (nonatomic,retain) id wrappedDelegate; 12 | @end 13 | 14 | @interface CDVHostedWebApp : CDVPlugin 15 | { 16 | CVDWebViewNotificationDelegate* notificationDelegate; 17 | NSDictionary* manifest; 18 | } 19 | 20 | @property (nonatomic, strong, readonly) NSDictionary* manifest; 21 | 22 | - (void)loadManifest:(CDVInvokedUrlCommand*)command; 23 | 24 | - (void)getManifest:(CDVInvokedUrlCommand*)command; 25 | 26 | - (void)enableOfflinePage:(CDVInvokedUrlCommand*)command; 27 | 28 | - (void)disableOfflinePage:(CDVInvokedUrlCommand*)command; 29 | 30 | - (void)injectPluginScript:(CDVInvokedUrlCommand*)command; 31 | 32 | @end 33 | -------------------------------------------------------------------------------- /src/ios/CDVHostedWebApp.m: -------------------------------------------------------------------------------- 1 | #import "CDVHostedWebApp.h" 2 | #import 3 | #import 4 | #import "CDVConnection.h" 5 | 6 | static NSString* const IOS_PLATFORM = @"ios"; 7 | static NSString* const DEFAULT_PLUGIN_MODE = @"client"; 8 | static NSString* const DEFAULT_CORDOVA_BASE_URL = @""; 9 | 10 | @interface CDVHostedWebApp () 11 | 12 | @property UIWebView *offlineView; 13 | @property NSString *offlinePage; 14 | @property NSString *manifestError; 15 | @property BOOL enableOfflineSupport; 16 | @property NSURL *failedURL; 17 | 18 | @end 19 | 20 | @implementation CVDWebViewNotificationDelegate 21 | 22 | - (void)webViewDidStartLoad:(UIWebView*)theWebView 23 | { 24 | [self.wrappedDelegate webViewDidStartLoad:theWebView]; 25 | 26 | [[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:kCDVHostedWebAppWebViewDidStartLoad object:theWebView]]; 27 | } 28 | 29 | - (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType 30 | { 31 | [[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:kCDVHostedWebAppWebViewShouldStartLoadWithRequest object:request]]; 32 | 33 | return [self.wrappedDelegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType]; 34 | } 35 | 36 | - (void)webViewDidFinishLoad:(UIWebView*)webView 37 | { 38 | [self.wrappedDelegate webViewDidFinishLoad:webView]; 39 | 40 | [[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:kCDVHostedWebAppWebViewDidFinishLoad object:webView]]; 41 | } 42 | 43 | - (void)webView:(UIWebView*)webView didFailLoadWithError:(NSError*)error 44 | { 45 | [self.wrappedDelegate webView:webView didFailLoadWithError:error]; 46 | 47 | [[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:kCDVHostedWebAppWebViewDidFailLoadWithError object:error]]; 48 | } 49 | 50 | @end 51 | 52 | @implementation CDVHostedWebApp 53 | 54 | @synthesize manifest; 55 | 56 | static NSString* const defaultManifestFileName = @"manifest.json"; 57 | 58 | - (void)pluginInitialize 59 | { 60 | [super pluginInitialize]; 61 | 62 | // observe notifications from network-information plugin to detect when device is offline 63 | [[NSNotificationCenter defaultCenter] addObserver:self 64 | selector:@selector(networkReachabilityChanged:) 65 | name:kReachabilityChangedNotification 66 | object:nil]; 67 | 68 | // observe notifications from webview when page starts loading 69 | [[NSNotificationCenter defaultCenter] addObserver:self 70 | selector:@selector(webViewDidStartLoad:) 71 | name:kCDVHostedWebAppWebViewDidStartLoad 72 | object:nil]; 73 | 74 | // observe notifications from webview when page starts loading 75 | [[NSNotificationCenter defaultCenter] addObserver:self 76 | selector:@selector(webViewDidFinishLoad:) 77 | name:kCDVHostedWebAppWebViewDidFinishLoad 78 | object:nil]; 79 | 80 | // observe notifications from webview when page fails loading 81 | [[NSNotificationCenter defaultCenter] addObserver:self 82 | selector:@selector(didWebViewFailLoadWithError:) 83 | name:kCDVHostedWebAppWebViewDidFailLoadWithError 84 | object:nil]; 85 | 86 | // observe notifications from app when it pauses 87 | [[NSNotificationCenter defaultCenter] addObserver:self 88 | selector:@selector(appStateChange) 89 | name:UIApplicationDidEnterBackgroundNotification 90 | object:nil]; 91 | 92 | // observe notifications from app when it resumes 93 | [[NSNotificationCenter defaultCenter] addObserver:self 94 | selector:@selector(appStateChange) 95 | name:UIApplicationWillEnterForegroundNotification 96 | object:nil]; 97 | 98 | // enable offline support by default 99 | self.enableOfflineSupport = YES; 100 | 101 | // no connection errors on startup 102 | self.failedURL = nil; 103 | 104 | // load the W3C manifest 105 | manifest = [self loadManifestFile:nil]; 106 | 107 | // set the webview delegate to notify navigation events 108 | notificationDelegate = [[CVDWebViewNotificationDelegate alloc] init]; 109 | notificationDelegate.wrappedDelegate = ((UIWebView*)self.webView).delegate; 110 | [(UIWebView*)self.webView setDelegate:notificationDelegate]; 111 | 112 | id offlineFeature = [manifest objectForKey:@"mjs_offline_feature"]; 113 | if (offlineFeature != nil && [offlineFeature boolValue] == NO) { 114 | self.enableOfflineSupport = NO; 115 | } else { 116 | // creates the UI to show offline mode 117 | [self createOfflineView]; 118 | } 119 | } 120 | 121 | // loads the specified W3C manifest 122 | - (void)loadManifest:(CDVInvokedUrlCommand*)command 123 | { 124 | CDVPluginResult* pluginResult = nil; 125 | NSString* manifestFileName = [command.arguments objectAtIndex:0]; 126 | 127 | manifest = [self loadManifestFile:manifestFileName]; 128 | if (self.manifest != nil) { 129 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:self.manifest]; 130 | } else { 131 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:self.manifestError]; 132 | } 133 | 134 | [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; 135 | } 136 | 137 | // returns the currently loaded manifest 138 | - (void)getManifest:(CDVInvokedUrlCommand*)command 139 | { 140 | CDVPluginResult* pluginResult = nil; 141 | if (self.manifest != nil) { 142 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:self.manifest]; 143 | } else { 144 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:self.manifestError]; 145 | } 146 | 147 | [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; 148 | } 149 | 150 | // enables offline page support 151 | - (void)enableOfflinePage:(CDVInvokedUrlCommand*)command 152 | { 153 | self.enableOfflineSupport = YES; 154 | CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsBool:true]; 155 | [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; 156 | } 157 | 158 | // disables offline page support 159 | - (void)disableOfflinePage:(CDVInvokedUrlCommand*)command 160 | { 161 | self.enableOfflineSupport = NO; 162 | CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsBool:true]; 163 | [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; 164 | } 165 | 166 | - (void)injectPluginScript:(CDVInvokedUrlCommand*)command 167 | { 168 | NSArray* scriptList = @[[command.arguments objectAtIndex:0]]; 169 | BOOL result = [self injectScripts:scriptList]; 170 | CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsBool:result]; 171 | [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; 172 | } 173 | 174 | // loads a manifest file and parses it 175 | - (NSDictionary*)loadManifestFile:(NSString*)manifestFileName 176 | { 177 | self.manifestError = nil; 178 | 179 | if (manifestFileName == nil) { 180 | manifestFileName = defaultManifestFileName; 181 | } 182 | 183 | NSString* filePath = [self.commandDelegate pathForResource:manifestFileName]; 184 | if (filePath == nil) { 185 | self.manifestError = [NSString stringWithFormat:@"Missing manifest file: %@", manifestFileName]; 186 | return nil; 187 | } 188 | 189 | NSData* manifestData = [NSData dataWithContentsOfFile:filePath]; 190 | if (manifestData == nil) { 191 | self.manifestError = [NSString stringWithFormat:@"Error reading manifest file: %@", manifestFileName]; 192 | return nil; 193 | } 194 | 195 | NSError* error = nil; 196 | id parsedManifest = [NSJSONSerialization JSONObjectWithData:manifestData options:0 error:&error]; 197 | 198 | if (error) { 199 | /* handle malformed JSON here */ 200 | self.manifestError = [NSString stringWithFormat:@"Error parsing manifest file: %@ - %@", manifestFileName, error]; 201 | return nil; 202 | } 203 | 204 | if([parsedManifest isKindOfClass:[NSDictionary class]]) { 205 | [[NSNotificationCenter defaultCenter] postNotificationName:kManifestLoadedNotification object:parsedManifest]; 206 | return parsedManifest; 207 | } 208 | 209 | /* deserialization is not a dictionary--it probably means an invalid manifest. */ 210 | self.manifestError = [NSString stringWithFormat:@"Invalid or unexpected manifest format: %@", manifestFileName]; 211 | return nil; 212 | } 213 | 214 | - (BOOL)injectScripts:(NSArray*)scriptList 215 | { 216 | NSString* content = @""; 217 | for (NSString* scriptName in scriptList) 218 | { 219 | NSURL* scriptUrl = [NSURL URLWithString:scriptName relativeToURL:[NSURL URLWithString:@"www/"]]; 220 | NSString* scriptPath = scriptUrl.absoluteString; 221 | NSError* error = nil; 222 | NSString* fileContents = nil; 223 | if (scriptUrl.scheme == nil) 224 | { 225 | fileContents = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:scriptPath ofType:nil] encoding:NSUTF8StringEncoding error:&error]; 226 | } 227 | else 228 | { 229 | fileContents = [NSString stringWithContentsOfURL:scriptUrl encoding:NSUTF8StringEncoding error:&error]; 230 | } 231 | 232 | if (error == nil) { 233 | // prefix with @ sourceURL= comment to make the injected scripts visible in Safari's Web Inspector for debugging purposes 234 | content = [content stringByAppendingFormat:@"\r\n//@ sourceURL=%@\r\n%@", scriptName, fileContents]; 235 | } 236 | else { 237 | NSLog(@"ERROR failed to load script file: '%@'", scriptName); 238 | } 239 | } 240 | 241 | return[(UIWebView*)self.webView stringByEvaluatingJavaScriptFromString:content] != nil; 242 | } 243 | 244 | - (BOOL)isCordovaEnabled 245 | { 246 | BOOL enableCordova = NO; 247 | NSObject* setting = [self.manifest objectForKey:@"mjs_api_access"]; 248 | if (setting != nil && [setting isKindOfClass:[NSArray class]]) 249 | { 250 | NSArray* accessRules = (NSArray*) setting; 251 | if (accessRules != nil) 252 | { 253 | for (NSDictionary* rule in accessRules) 254 | { 255 | if ([self isMatchingRuleForPage:rule withPlatformCheck:YES]) 256 | { 257 | setting = [rule objectForKey:@"access"]; 258 | 259 | NSString* access = setting != nil ? 260 | [(NSString*)setting stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] : nil; 261 | if (access == nil || [access isEqualToString:@"cordova"]) 262 | { 263 | enableCordova = YES; 264 | } 265 | else if ([access isEqualToString:@"none"]) 266 | { 267 | return NO; 268 | } 269 | else 270 | { 271 | NSLog(@"ERROR unsupported access type '%@' found in mjs_api_access rule.", access); 272 | } 273 | } 274 | } 275 | } 276 | } 277 | 278 | return enableCordova; 279 | } 280 | 281 | - (BOOL)isMatchingRuleForPage:(NSDictionary*)rule withPlatformCheck:(BOOL)checkPlatform 282 | { 283 | // ensure rule applies to current platform 284 | if (checkPlatform) 285 | { 286 | BOOL isPlatformMatch = NO; 287 | NSObject* setting = [rule objectForKey:@"platform"]; 288 | if (setting != nil && [setting isKindOfClass:[NSString class]]) 289 | { 290 | for (id item in [(NSString*)setting componentsSeparatedByString:@","]) 291 | { 292 | if ([[item stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] caseInsensitiveCompare:IOS_PLATFORM] == NSOrderedSame) 293 | { 294 | isPlatformMatch = YES; 295 | break; 296 | } 297 | } 298 | 299 | if (!isPlatformMatch) 300 | { 301 | return NO; 302 | } 303 | } 304 | } 305 | 306 | // ensure rule applies to current page 307 | BOOL isURLMatch = YES; 308 | NSObject* setting = [rule objectForKey:@"match"]; 309 | if (setting != nil) 310 | { 311 | NSArray* match = nil; 312 | if ([setting isKindOfClass:[NSArray class]]) 313 | { 314 | match = (NSArray*)setting; 315 | } 316 | else if ([setting isKindOfClass:[NSString class]]) 317 | { 318 | match = [NSArray arrayWithObjects:setting, nil]; 319 | } 320 | 321 | if (match != nil) 322 | { 323 | CDVWhitelist* whitelist = [[CDVWhitelist alloc] initWithArray:match]; 324 | NSURL* url = ((UIWebView*)self.webView).request.URL; 325 | isURLMatch = [whitelist URLIsAllowed:url]; 326 | } 327 | } 328 | 329 | return isURLMatch; 330 | } 331 | 332 | // Creates an additional webview to load the offline page, places it above the content webview, and hides it. It will 333 | // be made visible whenever network connectivity is lost. 334 | - (void)createOfflineView 335 | { 336 | CGRect webViewBounds = self.webView.bounds; 337 | 338 | webViewBounds.origin = self.webView.bounds.origin; 339 | 340 | self.offlineView = [[UIWebView alloc] initWithFrame:webViewBounds]; 341 | self.offlineView.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight); 342 | [self.offlineView setHidden:YES]; 343 | 344 | [self.viewController.view addSubview:self.offlineView]; 345 | 346 | NSURL* offlinePageURL = [NSURL URLWithString:self.offlinePage]; 347 | if (offlinePageURL == nil) { 348 | offlinePageURL = [NSURL URLWithString:@"offline.html"]; 349 | } 350 | 351 | NSString* offlineFilePath = [self.commandDelegate pathForResource:[offlinePageURL path]]; 352 | if (offlineFilePath != nil) { 353 | offlinePageURL = [NSURL fileURLWithPath:offlineFilePath]; 354 | [self.offlineView loadRequest:[NSURLRequest requestWithURL:offlinePageURL]]; 355 | } 356 | else { 357 | NSString* offlinePageTemplate = @"
%@
"; 358 | [self.offlineView 359 | loadHTMLString:[NSString stringWithFormat:offlinePageTemplate, @"It looks like you are offline. Please reconnect to use this application."] 360 | baseURL:nil]; 361 | } 362 | 363 | [self.viewController.view sendSubviewToBack:self.webView]; 364 | } 365 | 366 | - (void)networkReachabilityChanged:(NSNotification*)notification 367 | { 368 | if ([[notification name] isEqualToString:kReachabilityChangedNotification]) { 369 | CDVReachability* reachability = [notification object]; 370 | [self updateConnectivityStatus:reachability]; 371 | } 372 | } 373 | 374 | // Handles notifications from the network-information plugin and shows the offline page whenever 375 | // network connectivity is lost. It restores the original view once the network is up again. 376 | - (void)updateConnectivityStatus:(CDVReachability*)reachability 377 | { 378 | if ((reachability != nil) && [reachability isKindOfClass:[CDVReachability class]]) { 379 | BOOL isOffline = (reachability.currentReachabilityStatus == NotReachable); 380 | NSLog (@"Received a network connectivity change notification. The device is currently %@.", isOffline ? @"offLine" : @"online"); 381 | if (self.enableOfflineSupport) { 382 | if (isOffline) { 383 | [self.offlineView setHidden:NO]; 384 | } 385 | else { 386 | if (self.failedURL) { 387 | [(UIWebView*)self.webView loadRequest:[NSURLRequest requestWithURL:self.failedURL]]; 388 | } 389 | else { 390 | [self.offlineView setHidden:YES]; 391 | } 392 | } 393 | } 394 | } 395 | } 396 | 397 | // Handles notifications from the webview delegate whenever a page starts loading. 398 | - (void)webViewDidStartLoad:(NSNotification*)notification 399 | { 400 | if ([[notification name] isEqualToString:kCDVHostedWebAppWebViewDidStartLoad]) { 401 | NSLog (@"Received a navigation start notification."); 402 | self.failedURL = nil; 403 | } 404 | } 405 | 406 | // Handles notifications from the webview delegate whenever a page finishes loading. 407 | - (void)webViewDidFinishLoad:(NSNotification*)notification 408 | { 409 | if ([[notification name] isEqualToString:kCDVHostedWebAppWebViewDidFinishLoad]) { 410 | NSLog (@"Received a navigation completed notification."); 411 | if (!self.failedURL) { 412 | [self.offlineView setHidden:YES]; 413 | } 414 | 415 | // inject Cordova 416 | if ([self isCordovaEnabled]) 417 | { 418 | NSObject* setting = [self.manifest objectForKey:@"mjs_cordova"]; 419 | if (setting == nil && ![setting isKindOfClass:[NSDictionary class]]) 420 | { 421 | setting = [[NSDictionary alloc] init]; 422 | } 423 | 424 | NSDictionary* cordova = (NSDictionary*) setting; 425 | 426 | setting = [cordova objectForKey:@"plugin_mode"]; 427 | NSString* pluginMode = (setting != nil && [setting isKindOfClass:[NSString class]]) 428 | ? [(NSString*)setting stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] 429 | : DEFAULT_PLUGIN_MODE; 430 | 431 | setting = [cordova objectForKey:@"base_url"]; 432 | NSString* cordovaBaseUrl = (setting != nil && [setting isKindOfClass:[NSString class]]) 433 | ? [(NSString*)setting stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] 434 | : DEFAULT_CORDOVA_BASE_URL; 435 | 436 | if (![cordovaBaseUrl hasSuffix:@"/"]) 437 | { 438 | cordovaBaseUrl = [cordovaBaseUrl stringByAppendingString:@"/"]; 439 | } 440 | 441 | NSString* javascript = [NSString stringWithFormat:@"window.hostedWebApp = { 'platform': '%@', 'pluginMode': '%@', 'cordovaBaseUrl': '%@'};", IOS_PLATFORM, pluginMode, cordovaBaseUrl]; 442 | [(UIWebView*)self.webView stringByEvaluatingJavaScriptFromString:javascript]; 443 | 444 | NSMutableArray* scripts = [[NSMutableArray alloc] init]; 445 | if ([pluginMode isEqualToString:@"client"]) 446 | { 447 | [scripts addObject:@"cordova.js"]; 448 | } 449 | 450 | [scripts addObject:@"hostedapp-bridge.js"]; 451 | [self injectScripts:scripts]; 452 | } 453 | 454 | // inject custom scripts 455 | NSObject* setting = [self.manifest objectForKey:@"mjs_import_scripts"]; 456 | if (setting != nil && [setting isKindOfClass:[NSArray class]]) 457 | { 458 | NSArray* customScripts = (NSArray*)setting; 459 | if (customScripts != nil && customScripts.count > 0) 460 | { 461 | for (NSDictionary* item in customScripts) 462 | { 463 | if ([self isMatchingRuleForPage:item withPlatformCheck:NO]) 464 | { 465 | NSString* source = [item valueForKey:@"src"]; 466 | [self injectScripts:@[source]]; 467 | } 468 | } 469 | } 470 | } 471 | } 472 | } 473 | 474 | // Handles notifications from the webview delegate whenever a page load fails. 475 | - (void)didWebViewFailLoadWithError:(NSNotification*)notification 476 | { 477 | NSError* error = [notification object]; 478 | 479 | if ([[notification name] isEqualToString:kCDVHostedWebAppWebViewDidFailLoadWithError]) { 480 | NSLog (@"Received a navigation failure notification. error: %@", [error description]); 481 | if ([error code] == NSURLErrorTimedOut || 482 | [error code] == NSURLErrorUnsupportedURL || 483 | [error code] == NSURLErrorCannotFindHost || 484 | [error code] == NSURLErrorCannotConnectToHost || 485 | [error code] == NSURLErrorDNSLookupFailed || 486 | [error code] == NSURLErrorNotConnectedToInternet || 487 | [error code] == NSURLErrorNetworkConnectionLost) { 488 | 489 | self.failedURL = [NSURL URLWithString:[error.userInfo objectForKey:@"NSErrorFailingURLStringKey"]]; 490 | 491 | if (self.enableOfflineSupport) { 492 | [self.offlineView setHidden:NO]; 493 | } 494 | } 495 | } 496 | } 497 | 498 | #ifndef __CORDOVA_4_0_0 499 | - (BOOL)shouldOverrideLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType 500 | { 501 | NSURL* url = [request URL]; 502 | 503 | if (![self shouldAllowNavigation:url]) 504 | { 505 | if ([[UIApplication sharedApplication] canOpenURL:url]) 506 | { 507 | [[UIApplication sharedApplication] openURL:url]; // opens the URL outside the webview 508 | return YES; 509 | } 510 | } 511 | 512 | return NO; 513 | } 514 | 515 | - (BOOL)shouldAllowNavigation:(NSURL*)url 516 | { 517 | NSMutableArray* scopeList = [[NSMutableArray alloc] initWithCapacity:0]; 518 | 519 | // determine base rule based on the start_url and the scope 520 | NSURL* baseURL = nil; 521 | NSString* startURL = [self.manifest objectForKey:@"start_url"]; 522 | if (startURL != nil) { 523 | baseURL = [NSURL URLWithString:startURL]; 524 | NSString* scope = [self.manifest objectForKey:@"scope"]; 525 | if (scope != nil) { 526 | baseURL = [NSURL URLWithString:scope relativeToURL:baseURL]; 527 | } 528 | } 529 | 530 | if (baseURL != nil) { 531 | // If there are no wildcards in the pattern, add '*' at the end 532 | if (![[baseURL absoluteString] containsString:@"*"]) { 533 | baseURL = [NSURL URLWithString:@"*" relativeToURL:baseURL]; 534 | } 535 | 536 | 537 | // add base rule to the scope list 538 | [scopeList addObject:[baseURL absoluteString]]; 539 | } 540 | 541 | // add additional navigation rules from mjs_access_whitelist 542 | // TODO: mjs_access_whitelist is deprecated. Should be removed in future versions 543 | NSObject* setting = [self.manifest objectForKey:@"mjs_access_whitelist"]; 544 | if (setting != nil && [setting isKindOfClass:[NSArray class]]) 545 | { 546 | NSArray* accessRules = (NSArray*)setting; 547 | if (accessRules != nil) 548 | { 549 | for (NSDictionary* rule in accessRules) 550 | { 551 | NSString* accessUrl = [rule objectForKey:@"url"]; 552 | if (accessUrl != nil) 553 | { 554 | [scopeList addObject:accessUrl]; 555 | } 556 | } 557 | } 558 | } 559 | 560 | // add additional navigation rules from mjs_extended_scope 561 | setting = [self.manifest objectForKey:@"mjs_extended_scope"]; 562 | if (setting != nil && [setting isKindOfClass:[NSArray class]]) 563 | { 564 | NSArray* scopeRules = (NSArray*)setting; 565 | if (scopeRules != nil) 566 | { 567 | for (NSString* rule in scopeRules) 568 | { 569 | [scopeList addObject:rule]; 570 | } 571 | } 572 | } 573 | 574 | return [[[CDVWhitelist alloc] initWithArray:scopeList] URLIsAllowed:url]; 575 | } 576 | #endif 577 | 578 | // Updates the network connectivity status when the app is paused or resumes 579 | // NOTE: for onPause and onResume, calls into JavaScript must not call or trigger any blocking UI, like alerts 580 | - (void)appStateChange 581 | { 582 | CDVConnection* connection = [self.commandDelegate getCommandInstance:@"NetworkStatus"]; 583 | [self updateConnectivityStatus:connection.internetReach]; 584 | } 585 | 586 | @end 587 | -------------------------------------------------------------------------------- /src/windows/HostedWebAppPluginProxy.js: -------------------------------------------------------------------------------- 1 | var _manifest; 2 | var _manifestError; 3 | var _offlineView; 4 | var _mainView; 5 | var _zIndex = 10000; 6 | var _enableOfflineSupport = true; 7 | var _lastKnownLocation; 8 | var _lastKnownLocationFailed = false; 9 | var _whiteList = []; 10 | 11 | function bridgeNativeEvent(e) { 12 | _mainView.invokeScriptAsync('eval', "cordova && cordova.fireDocumentEvent('" + e.type + "', null, true);").start(); 13 | } 14 | 15 | //document.addEventListener('backbutton', bridgeNativeEvent, false); 16 | document.addEventListener('pause', bridgeNativeEvent, false); 17 | document.addEventListener('resume', bridgeNativeEvent, false); 18 | 19 | // creates a webview to host content 20 | function configureHost(url, zOrder, display) { 21 | var webView = document.createElement(cordova.platformId === 'windows8' ? 'iframe' : 'x-ms-webview'); 22 | var style = webView.style; 23 | style.position = 'absolute'; 24 | style.top = 0; 25 | style.left = 0; 26 | style.zIndex = zOrder; 27 | style.width = '100%'; 28 | style.height = '100%'; 29 | if (display) { 30 | style.display = display; 31 | } 32 | 33 | if (url) { 34 | webView.src = url; 35 | } 36 | 37 | webView.addEventListener("MSWebViewNavigationCompleted", navigationCompletedEvent, false); 38 | webView.addEventListener("MSWebViewNavigationStarting", navigationStartingEvent, false); 39 | 40 | document.body.appendChild(webView); 41 | 42 | return webView; 43 | } 44 | 45 | // handles webview's navigation starting event 46 | function navigationStartingEvent(evt) { 47 | if (handleCordovaExecCalls(evt)) { 48 | evt.stopImmediatePropagation(); 49 | evt.preventDefault(); 50 | return; 51 | } 52 | 53 | if (evt.uri && evt.uri !== "") { 54 | var isInWhitelist = false; 55 | for (var i = 0; i < _whiteList.length; i++) { 56 | var rule = _whiteList[i]; 57 | if (rule.test(evt.uri)) { 58 | isInWhitelist = true; 59 | break; 60 | } 61 | } 62 | 63 | // if the url to navigate to does not match any of the rules in the whitelist, open it outside de app 64 | if (!isInWhitelist) { 65 | evt.stopImmediatePropagation(); 66 | evt.preventDefault(); 67 | console.log("Whitelist rejection: url='" + evt.uri + "'"); 68 | Windows.System.Launcher.launchUriAsync(new Windows.Foundation.Uri(evt.uri)); 69 | } 70 | } 71 | } 72 | 73 | // handles webview's navigation completed event 74 | function navigationCompletedEvent(evt) { 75 | if (evt.uri && evt.uri !== "") { 76 | if (evt.isSuccess) { 77 | _lastKnownLocationFailed = false; 78 | if (_offlineView) { 79 | _offlineView.style.display = 'none'; 80 | } 81 | } else { 82 | _lastKnownLocationFailed = true; 83 | } 84 | 85 | _lastKnownLocation = evt.uri; 86 | } 87 | } 88 | 89 | function domContentLoadedEvent(evt) { 90 | console.log('Finished loading URL: ' + _mainView.src); 91 | 92 | hideExtendedSplashScreen(); 93 | 94 | // inject Cordova 95 | if (isCordovaEnabled()) { 96 | var cordova = _manifest.mjs_cordova || {}; 97 | 98 | var pluginMode = cordova.plugin_mode || 'client'; 99 | var cordovaBaseUrl = (cordova.base_url || '').trim(); 100 | if (cordovaBaseUrl.indexOf('/', cordovaBaseUrl.length - 1) === -1) { 101 | cordovaBaseUrl += '/'; 102 | } 103 | 104 | _mainView.invokeScriptAsync('eval', 'window.hostedWebApp = { \'platform\': \'windows\', \'pluginMode\': \'' + pluginMode + '\', \'cordovaBaseUrl\': \'' + cordovaBaseUrl + '\'};').start(); 105 | 106 | var scriptsToInject = []; 107 | if (pluginMode === 'client') { 108 | scriptsToInject.push('cordova.js'); 109 | } 110 | 111 | scriptsToInject.push('hostedapp-bridge.js'); 112 | injectScripts(scriptsToInject); 113 | } 114 | 115 | // inject import scripts 116 | if (_manifest && _manifest.mjs_import_scripts && _manifest.mjs_import_scripts instanceof Array) { 117 | var scriptFiles = _manifest.mjs_import_scripts 118 | .filter(isMatchingRuleForPage) 119 | .map(function (item) { 120 | return item.src; 121 | }); 122 | 123 | if (scriptFiles.length) { 124 | injectScripts(scriptFiles); 125 | } 126 | } 127 | } 128 | 129 | // checks if Cordova runtime environment is enabled for the current page 130 | function isCordovaEnabled() { 131 | var allow = true; 132 | var enableCordova = false; 133 | var accessRules = _manifest.mjs_api_access; 134 | if (accessRules) { 135 | accessRules.forEach(function (rule) { 136 | if (isMatchingRuleForPage(rule, true)) { 137 | var access = rule.access; 138 | if (!access || access === 'cordova') { 139 | enableCordova = true; 140 | } 141 | else if (access === 'none') { 142 | allow = false; 143 | } 144 | else { 145 | console.log('Unsupported API access type \'' + access + '\' found in mjs_api_access rule.'); 146 | } 147 | } 148 | }); 149 | } 150 | 151 | return enableCordova && allow; 152 | } 153 | 154 | // check if an API access or custom script match rule applies to the current page 155 | function isMatchingRuleForPage(rule, checkPlatform) { 156 | 157 | // ensure rule applies to current platform 158 | if (checkPlatform) { 159 | if (rule.platform && rule.platform.split(',') 160 | .map(function (item) { return item.trim(); }) 161 | .indexOf('windows') < 0) { 162 | return false; 163 | } 164 | } 165 | 166 | // ensure rule applies to current page 167 | var match = rule.match; 168 | if (match) { 169 | if (typeof match === 'string' && match.length) { 170 | match = [match]; 171 | } 172 | 173 | return match.some(function (item) { return convertPatternToRegex(item).test(_mainView.src); }); 174 | } 175 | 176 | return true; 177 | } 178 | 179 | // handles network connectivity change events 180 | function connectivityEvent(evt) { 181 | console.log('Received a network connectivity change notification. The device is currently ' + evt.type + '.'); 182 | if (_enableOfflineSupport) { 183 | if (evt.type === 'offline') { 184 | _offlineView.style.display = 'block'; 185 | } else if (evt.type === 'online') { 186 | if (_lastKnownLocationFailed) { 187 | if (_lastKnownLocation) { 188 | console.log("Reload last known location: '" + _lastKnownLocation + "'"); 189 | _mainView.src = _lastKnownLocation; 190 | } 191 | } else { 192 | _offlineView.style.display = 'none'; 193 | } 194 | } 195 | } 196 | } 197 | 198 | // sets up a secondary webview to host the offline page 199 | function configureOfflineSupport(offlinePage) { 200 | var offlinePageUrl = '///www/' + offlinePage; 201 | var url = new Windows.Foundation.Uri('ms-appx:' + offlinePageUrl); 202 | Windows.Storage.StorageFile.getFileFromApplicationUriAsync(url).then( 203 | function (file) { 204 | _offlineView = configureHost('ms-appx-web:' + offlinePageUrl, 10001, 'none'); 205 | }, 206 | function (err) { 207 | var message = 'It looks like you are offline. Please reconnect to use this application.'; 208 | var offlinePageTemplate = '
' + message + '
'; 209 | _offlineView = configureHost(null, _zIndex + 1, 'none'); 210 | 211 | if (cordova.platformId === 'windows8') { 212 | _offlineView.style.backgroundColor = 'white'; 213 | _offlineView.contentDocument.write(offlinePageTemplate); 214 | } else { 215 | _offlineView.navigateToString(offlinePageTemplate); 216 | } 217 | }).done(function () { 218 | document.addEventListener('offline', connectivityEvent, false); 219 | document.addEventListener('online', connectivityEvent, false); 220 | }); 221 | } 222 | 223 | // escapes regular expression reserved symbols 224 | function escapeRegex(str) { 225 | return ("" + str).replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1"); 226 | } 227 | 228 | // converts a string pattern to a regular expression 229 | function convertPatternToRegex(pattern, excludeLineStart, excludeLineEnd) { 230 | var isNot = (pattern[0] == '!'); 231 | if (isNot) { pattern = pattern.substr(1) }; 232 | 233 | var regexBody = escapeRegex(pattern); 234 | 235 | excludeLineStart = !!excludeLineStart; 236 | excludeLineEnd = !!excludeLineEnd; 237 | 238 | regexBody = regexBody.replace(/\\\?/g, ".?").replace(/\\\*/g, ".*?"); 239 | if (isNot) { regexBody = "((?!" + regexBody + ").)*"; } 240 | if (!excludeLineStart) { regexBody = "^" + regexBody; } 241 | if (!excludeLineEnd) { regexBody += "\/?$"; } 242 | 243 | return new RegExp(regexBody); 244 | } 245 | 246 | // initializes an array of the access rules defined in the manifest 247 | function configureWhiteList(manifest) { 248 | if (manifest) { 249 | // Add base access rule based on the start_url and the scope 250 | var baseUrlPattern = new Windows.Foundation.Uri(manifest.start_url); 251 | if (manifest.scope && manifest.scope.length) { 252 | baseUrlPattern = baseUrlPattern.combineUri(manifest.scope); 253 | } 254 | 255 | baseUrlPattern = baseUrlPattern.combineUri('*'); 256 | _whiteList.push(convertPatternToRegex(baseUrlPattern.absoluteUri)); 257 | 258 | // add additional access rules from mjs_access_whitelist 259 | // TODO: mjs_access_whitelist is deprecated. Should be removed in future versions 260 | if (manifest.mjs_access_whitelist && manifest.mjs_access_whitelist instanceof Array) { 261 | manifest.mjs_access_whitelist.forEach(function (rule) { 262 | _whiteList.push(convertPatternToRegex(rule.url)); 263 | }); 264 | } 265 | 266 | // add additional access rules from mjs_extended_scope 267 | if (manifest.mjs_extended_scope && manifest.mjs_extended_scope instanceof Array) { 268 | manifest.mjs_extended_scope.forEach(function (rule) { 269 | _whiteList.push(convertPatternToRegex(rule)); 270 | }); 271 | } 272 | } 273 | } 274 | 275 | // hides the extended splash screen 276 | function hideExtendedSplashScreen() { 277 | var extendedSplashScreen = document.getElementById("extendedSplashScreen"); 278 | extendedSplashScreen.style.display = "none"; 279 | } 280 | 281 | // handle the hardware backbutton 282 | function navigateBack(e) { 283 | if (!_mainView.canGoBack) { 284 | return false; 285 | } 286 | 287 | try { 288 | _mainView.goBack(); 289 | } catch (err) { 290 | return false; 291 | } 292 | 293 | return true; 294 | } 295 | 296 | var exec = require('cordova/exec'); 297 | 298 | function injectScripts(files, successCallback, errorCallback) { 299 | 300 | var script = (arguments.length > 3 && typeof arguments[3] === 'string') ? arguments[3] : ''; 301 | var fileList = (arguments.length > 4 && typeof arguments[4] === 'string') ? arguments[4] : ''; 302 | 303 | if (typeof files === 'string') { 304 | files = files.length ? [files] : []; 305 | } 306 | 307 | var fileName = files.shift(); 308 | if (!fileName) { 309 | var asyncOp = _mainView.invokeScriptAsync('eval', script); 310 | asyncOp.oncomplete = function () { successCallback && successCallback(true); }; 311 | asyncOp.onerror = function (err) { 312 | console.log('Error injecting script file(s): ' + fileList + ' - ' + asyncOp.error); 313 | errorCallback && errorCallback(err); 314 | }; 315 | 316 | asyncOp.start() 317 | return; 318 | } 319 | 320 | console.log('Injecting script file: ' + fileName); 321 | var uri = new Windows.Foundation.Uri('ms-appx:///www/', fileName); 322 | 323 | var onSuccess = function (content) { 324 | script += '\r\n//# sourceURL=' + fileName + '\r\n' + content; 325 | injectScripts(files, successCallback, errorCallback, script, (fileList ? ', ' : '') + fileName); 326 | }; 327 | 328 | var onError = function (err) { 329 | console.log('Error retrieving script file from app package: ' + fileName + ' - ' + err); 330 | if (errorCallback) { 331 | errorCallback(err); 332 | } 333 | }; 334 | 335 | if (uri.schemeName == 'ms-appx') { 336 | Windows.Storage.StorageFile.getFileFromApplicationUriAsync(uri) 337 | .done(function (file) { 338 | Windows.Storage.FileIO.readTextAsync(file) 339 | .done(onSuccess, onError); 340 | }, onError); 341 | } else { 342 | var httpClient = new Windows.Web.Http.HttpClient(); 343 | httpClient.getStringAsync(uri).done(onSuccess, onError); 344 | httpClient.close(); 345 | } 346 | } 347 | 348 | function handleCordovaExecCalls(evt) { 349 | if (evt.uri) { 350 | var targetUri = new Windows.Foundation.Uri(evt.uri); 351 | if (targetUri.host === '.cordova' && targetUri.path === '/exec') { 352 | var service = targetUri.queryParsed.getFirstValueByName('service'); 353 | var action = targetUri.queryParsed.getFirstValueByName('action'); 354 | var args = JSON.parse(decodeURIComponent(targetUri.queryParsed.getFirstValueByName('args'))); 355 | var callbackId = targetUri.queryParsed.getFirstValueByName('callbackId'); 356 | 357 | var success, fail; 358 | if (callbackId !== '0') { 359 | success = function (args) { 360 | var params = args ? '"' + encodeURIComponent(JSON.stringify(args)) + '"' : ''; 361 | var script = 'cordova.callbacks["' + callbackId + '"].success(' + params + ');'; 362 | _mainView.invokeScriptAsync('eval', script).start(); 363 | }; 364 | 365 | fail = function (err) { 366 | var params = args ? '"' + encodeURIComponent(JSON.stringify(err)) + '"' : ''; 367 | var script = 'cordova.callbacks["' + callbackId + '"].fail(' + params + ');'; 368 | _mainView.invokeScriptAsync('eval', script).start(); 369 | }; 370 | } 371 | 372 | exec(success, fail, service, action, args); 373 | 374 | return true; 375 | } 376 | } 377 | 378 | return false; 379 | } 380 | 381 | module.exports = { 382 | // loads the W3C manifest file and parses it 383 | loadManifest: function (successCallback, errorCallback, args) { 384 | var manifestFileName = (args && args instanceof Array && args.length > 0) ? args[0] : 'manifest.json'; 385 | var configFile = 'ms-appx:///www/' + manifestFileName; 386 | var uri = new Windows.Foundation.Uri(configFile); 387 | Windows.Storage.StorageFile.getFileFromApplicationUriAsync(uri).then( 388 | function (file) { 389 | Windows.Storage.FileIO.readTextAsync(file).then(function (data) { 390 | try { 391 | _manifest = JSON.parse(data); 392 | cordova.fireDocumentEvent("manifestLoaded", { manifest: _manifest }); 393 | successCallback && successCallback(_manifest); 394 | } catch (err) { 395 | _manifestError = 'Error parsing manifest file: ' + manifestFileName + ' - ' + err.message; 396 | console.log(_manifestError); 397 | } 398 | }); 399 | }, 400 | function (err) { 401 | _manifestError = 'Error reading manifest file: ' + manifestFileName + ' - ' + err; 402 | console.log(_manifestError); 403 | errorCallback && errorCallback(err); 404 | }); 405 | }, 406 | 407 | // returns the currently loaded manifest 408 | getManifest: function (successCallback, errorCallback) { 409 | if (_manifest) { 410 | successCallback && successCallback(_manifest); 411 | } else { 412 | errorCallback && errorCallback(new Error(_manifestError)); 413 | } 414 | }, 415 | 416 | // enables offline page support 417 | enableOfflinePage: function (successCallback, errorCallback) { 418 | _enableOfflineSupport = true; 419 | successCallback && successCallback(); 420 | }, 421 | 422 | // disables offline page support 423 | disableOfflinePage: function (successCallback, errorCallback) { 424 | _enableOfflineSupport = false; 425 | successCallback && successCallback(); 426 | }, 427 | 428 | getWebView: function () { 429 | return _mainView; 430 | }, 431 | 432 | injectPluginScript: function (successCallback, errorCallback, file) { 433 | injectScripts(file, successCallback, errorCallback); 434 | } 435 | }; // exports 436 | 437 | cordova.commandProxy.add('HostedWebApp', module.exports); 438 | 439 | module.exports.loadManifest( 440 | function (manifest) { 441 | if (manifest.mjs_offline_feature === false) { 442 | _enableOfflineSupport = false; 443 | } else { 444 | configureOfflineSupport('offline.html'); 445 | } 446 | configureWhiteList(manifest); 447 | _mainView = configureHost(manifest ? manifest.start_url : 'about:blank', _zIndex); 448 | _mainView.addEventListener("MSWebViewDOMContentLoaded", domContentLoadedEvent, false); 449 | 450 | cordova.fireDocumentEvent("webviewCreated", { webView: _mainView }); 451 | WinJS.Application.onbackclick = navigateBack; 452 | }); 453 | -------------------------------------------------------------------------------- /www/hostedWebApp.js: -------------------------------------------------------------------------------- 1 | var hostedwebapp = { 2 | loadManifest: function (successCallback, errorCallback, manifestFileName) { 3 | cordova.exec(successCallback, errorCallback, "HostedWebApp", "loadManifest", [manifestFileName]); 4 | }, 5 | getManifest: function (successCallback, errorCallback) { 6 | cordova.exec(successCallback, errorCallback, "HostedWebApp", "getManifest", []); 7 | }, 8 | enableOfflinePage : function () { 9 | cordova.exec(undefined, undefined, "HostedWebApp", "enableOfflinePage", []); 10 | }, 11 | disableOfflinePage : function () { 12 | cordova.exec(undefined, undefined, "HostedWebApp", "disableOfflinePage", []); 13 | } 14 | } 15 | 16 | module.exports = hostedwebapp; 17 | -------------------------------------------------------------------------------- /www/hostedapp-bridge.js: -------------------------------------------------------------------------------- 1 | (function (platform, pluginMode, cordovaBaseUrl) { 2 | function onCordovaLoaded() { 3 | var channel = cordova.require('cordova/channel'); 4 | channel.onNativeReady.subscribe(function () { 5 | 6 | // for Windows plaftform, redefine exec to bridge calls to exec on "native" side (i.e. webview container) 7 | if (platform === 'windows') { 8 | cordova.define.remove('cordova/exec'); 9 | cordova.define('cordova/exec', function (require, exports, module) { 10 | module.exports = function (completeCallback, failureCallback, service, action, args) { 11 | var success, fail; 12 | 13 | var command = 'http://.cordova/exec?service=' + service + '&action=' + action + '&args=' + encodeURIComponent(JSON.stringify(args)); 14 | 15 | if (typeof completeCallback === 'function') { 16 | success = function (args) { 17 | var result = args ? JSON.parse(decodeURIComponent(args)) : undefined; 18 | completeCallback(result); 19 | }; 20 | } 21 | 22 | if (typeof failureCallback === 'function') { 23 | fail = function (args) { 24 | var err = args ? JSON.parse(decodeURIComponent(args)) : undefined; 25 | failureCallback(err); 26 | }; 27 | } 28 | 29 | var callbackId = 0; 30 | if (success || fail) { 31 | callbackId = service + cordova.callbackId++; 32 | cordova.callbacks[callbackId] = { success: success, fail: fail }; 33 | } 34 | 35 | command += '&callbackId=' + encodeURIComponent(callbackId); 36 | 37 | window.location.href = command; 38 | }; 39 | }); 40 | } 41 | 42 | // change bridge mode in iOS to avoid Content Security Policy (CSP) issues with 'gap://' frame origin 43 | // Note that all bridge modes except IFRAME_NAV were dropped starting from cordova-ios@4.0.0 (see 44 | // https://issues.apache.org/jira/browse/CB-9883), so plugins will *not* work correctly in pages that 45 | // restrict the gap:// origin 46 | if (platform === 'ios') { 47 | var exec = cordova.require('cordova/exec'); 48 | if (exec.setJsToNativeBridgeMode && exec.jsToNativeModes && exec.jsToNativeModes.XHR_OPTIONAL_PAYLOAD) { 49 | exec.setJsToNativeBridgeMode(exec.jsToNativeModes.XHR_OPTIONAL_PAYLOAD); 50 | } 51 | } 52 | 53 | // override plugin loader to handle script injection 54 | var pluginloader = cordova.require('cordova/pluginloader'); 55 | var defaultInjectScript = pluginloader.injectScript; 56 | pluginloader.injectScript = function (url, onload, onerror) { 57 | 58 | var onloadHandler = onload, onerrorHandler = onerror; 59 | 60 | // check if script being injected is 'cordova_plugins.js' 61 | var cordovaPluginsScript = 'cordova_plugins.js'; 62 | if (url.indexOf(cordovaPluginsScript, url.length - cordovaPluginsScript.length) !== -1) { 63 | 64 | // In Windows platform, avoid loading scripts from the "native" side 65 | if (platform === 'windows') { 66 | 67 | // redefine onload to exclude scripts in 'www' folder 68 | onloadHandler = function () { 69 | var moduleList = cordova.require('cordova/plugin_list'); 70 | for (var i = moduleList.length - 1; i >= 0; i--) { 71 | if (moduleList[i].file.indexOf('/www/') < 0) { 72 | moduleList.splice(i, 1); 73 | } 74 | } 75 | 76 | onload(); 77 | }; 78 | } 79 | 80 | // In server mode, rewrite url to retrieve platform specific file 81 | if (pluginMode === 'server') { 82 | url = url.replace(cordovaPluginsScript, 'cordova_plugins-' + platform + '.js'); 83 | } 84 | } 85 | 86 | // In client mode, call native side to load and inject the script from the app package 87 | if (pluginMode === 'client') { 88 | return cordova.require('cordova/exec')(function (result) { 89 | 90 | // native side did not handle the script--using default mechanism 91 | if (!result) { 92 | return defaultInjectScript(url, onloadHandler, onerrorHandler); 93 | } 94 | 95 | onloadHandler(); 96 | }, 97 | function (err) { 98 | onerrorHandler(err); 99 | }, 100 | 'HostedWebApp', 'injectPluginScript', [url]); 101 | } 102 | 103 | if (pluginMode === 'server') { 104 | url = cordovaBaseUrl + url; 105 | } 106 | 107 | defaultInjectScript(url, onloadHandler, onerrorHandler); 108 | }; 109 | }); 110 | } 111 | 112 | // inject the platform specific cordova.js file 113 | if (pluginMode === 'server') { 114 | function injectScript(url, onload) { 115 | var script = document.createElement('script'); 116 | script.src = url; 117 | script.onload = onload; 118 | document.head.appendChild(script); 119 | } 120 | 121 | var cordovaSrc = cordovaBaseUrl + 'cordova-' + platform + '.js'; 122 | injectScript(cordovaSrc, onCordovaLoaded); 123 | } else { 124 | onCordovaLoaded(); 125 | } 126 | 127 | })(window.hostedWebApp.platform, window.hostedWebApp.pluginMode, window.hostedWebApp.cordovaBaseUrl); 128 | --------------------------------------------------------------------------------