├── .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 |
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 |
--------------------------------------------------------------------------------