Compare commits

...

8 Commits

Author SHA1 Message Date
Ian Clelland
198364afb9 Update native tests 2014-10-30 15:08:26 -04:00
Ian Clelland
ed78b557cd Remove whitelist config.xml parsing 2014-10-30 12:19:06 -04:00
Ian Clelland
83377d366a Remove whitelists from WebView classes 2014-10-30 12:19:06 -04:00
Ian Clelland
8df2d4fcfd Remove unused Config methods (Breaking Change) 2014-10-30 12:19:06 -04:00
Ian Clelland
a0acb4ce9a Refactor ConfigXmlParser to allow subclasses 2014-10-30 12:19:06 -04:00
Ian Clelland
fe15d34a80 Use /app_webview/ rather than app_webview to filter bad requests 2014-10-30 12:19:06 -04:00
Ian Clelland
23584274d2 Defer whitelist decisions to plugins
There is a default policy, which is implemented in the case where no plugins override any of the whitelist methods:
 * Error URLs must start with file://
 * Navigation is allowed to file:// and data: URLs which do not contain "app_webview"
 * External URLs do not launch intents
 * XHRs are allowed to file:// and data: URLs which do not contain "app_webview"
2014-10-30 12:19:06 -04:00
Ian Clelland
44aa98887f Add hooks in CordovaPlugin and PluginManager for whitelist plugins
This adds three hooks to CordovaPlugin objects. In each case, a null
value can be returned to indicate "I don't care". This null value is
the default.

    public Boolean shouldAllowRequest(String url)
    public Boolean shouldAllowNavigation(String url)
    public Boolean shouldOpenExternalUrl(String url)
2014-10-30 12:19:05 -04:00
12 changed files with 305 additions and 189 deletions

View File

@ -86,9 +86,8 @@ public class AndroidWebView extends WebView implements CordovaWebView {
private WebChromeClient.CustomViewCallback mCustomViewCallback; private WebChromeClient.CustomViewCallback mCustomViewCallback;
private CordovaResourceApi resourceApi; private CordovaResourceApi resourceApi;
private Whitelist internalWhitelist;
private Whitelist externalWhitelist;
private CordovaPreferences preferences; private CordovaPreferences preferences;
private CordovaUriHelper helper;
// The URL passed to loadUrl(), not necessarily the URL of the current page. // The URL passed to loadUrl(), not necessarily the URL of the current page.
String loadedUrl; String loadedUrl;
@ -111,19 +110,17 @@ public class AndroidWebView extends WebView implements CordovaWebView {
// Use two-phase init so that the control will work with XML layouts. // Use two-phase init so that the control will work with XML layouts.
@Override @Override
public void init(CordovaInterface cordova, List<PluginEntry> pluginEntries, public void init(CordovaInterface cordova, List<PluginEntry> pluginEntries,
Whitelist internalWhitelist, Whitelist externalWhitelist,
CordovaPreferences preferences) { CordovaPreferences preferences) {
if (this.cordova != null) { if (this.cordova != null) {
throw new IllegalStateException(); throw new IllegalStateException();
} }
this.cordova = cordova; this.cordova = cordova;
this.internalWhitelist = internalWhitelist;
this.externalWhitelist = externalWhitelist;
this.preferences = preferences; this.preferences = preferences;
this.helper = new CordovaUriHelper(cordova, this);
pluginManager = new PluginManager(this, this.cordova, pluginEntries); pluginManager = new PluginManager(this, this.cordova, pluginEntries);
resourceApi = new CordovaResourceApi(this.getContext(), pluginManager); resourceApi = new CordovaResourceApi(this.getContext(), pluginManager);
bridge = new CordovaBridge(pluginManager, new NativeToJsMessageQueue(this, cordova)); bridge = new CordovaBridge(pluginManager, new NativeToJsMessageQueue(this, cordova), helper);
pluginManager.addService("App", "org.apache.cordova.CoreAndroid"); pluginManager.addService("App", "org.apache.cordova.CoreAndroid");
initWebViewSettings(); initWebViewSettings();
@ -354,7 +351,7 @@ public class AndroidWebView extends WebView implements CordovaWebView {
if (LOG.isLoggable(LOG.DEBUG) && !url.startsWith("javascript:")) { if (LOG.isLoggable(LOG.DEBUG) && !url.startsWith("javascript:")) {
LOG.d(TAG, ">>> loadUrlNow()"); LOG.d(TAG, ">>> loadUrlNow()");
} }
if (url.startsWith("file://") || url.startsWith("javascript:") || internalWhitelist.isUrlWhiteListed(url)) { if (url.startsWith("javascript:") || helper.shouldAllowNavigation(url)) {
super.loadUrl(url); super.loadUrl(url);
} }
} }
@ -429,7 +426,7 @@ public class AndroidWebView extends WebView implements CordovaWebView {
if (!openExternal) { if (!openExternal) {
// Make sure url is in whitelist // Make sure url is in whitelist
if (url.startsWith("file://") || internalWhitelist.isUrlWhiteListed(url)) { if (helper.shouldAllowNavigation(url)) {
// TODO: What about params? // TODO: What about params?
// Load new URL // Load new URL
loadUrlIntoView(url, true); loadUrlIntoView(url, true);
@ -748,16 +745,6 @@ public class AndroidWebView extends WebView implements CordovaWebView {
return this; return this;
} }
@Override
public Whitelist getWhitelist() {
return this.internalWhitelist;
}
@Override
public Whitelist getExternalWhitelist() {
return this.externalWhitelist;
}
@Override @Override
public CordovaPreferences getPreferences() { public CordovaPreferences getPreferences() {
return preferences; return preferences;

View File

@ -46,48 +46,6 @@ public class Config {
parser = new ConfigXmlParser(); parser = new ConfigXmlParser();
} }
} }
/**
* Add entry to approved list of URLs (whitelist)
*
* @param origin URL regular expression to allow
* @param subdomains T=include all subdomains under origin
*/
public static void addWhiteListEntry(String origin, boolean subdomains) {
if (parser == null) {
Log.e(TAG, "Config was not initialised. Did you forget to Config.init(this)?");
return;
}
parser.getInternalWhitelist().addWhiteListEntry(origin, subdomains);
}
/**
* Determine if URL is in approved list of URLs to load.
*
* @param url
* @return true if whitelisted
*/
public static boolean isUrlWhiteListed(String url) {
if (parser == null) {
Log.e(TAG, "Config was not initialised. Did you forget to Config.init(this)?");
return false;
}
return parser.getInternalWhitelist().isUrlWhiteListed(url);
}
/**
* Determine if URL is in approved list of URLs to launch external applications.
*
* @param url
* @return true if whitelisted
*/
public static boolean isUrlExternallyWhiteListed(String url) {
if (parser == null) {
Log.e(TAG, "Config was not initialised. Did you forget to Config.init(this)?");
return false;
}
return parser.getExternalWhitelist().isUrlWhiteListed(url);
}
public static String getStartUrl() { public static String getStartUrl() {
if (parser == null) { if (parser == null) {
@ -100,14 +58,6 @@ public class Config {
return parser.getPreferences().getString("errorurl", null); return parser.getPreferences().getString("errorurl", null);
} }
public static Whitelist getWhitelist() {
return parser.getInternalWhitelist();
}
public static Whitelist getExternalWhitelist() {
return parser.getExternalWhitelist();
}
public static List<PluginEntry> getPluginEntries() { public static List<PluginEntry> getPluginEntries() {
return parser.getPluginEntries(); return parser.getPluginEntries();
} }

View File

@ -36,18 +36,8 @@ public class ConfigXmlParser {
private String launchUrl = "file:///android_asset/www/index.html"; private String launchUrl = "file:///android_asset/www/index.html";
private CordovaPreferences prefs = new CordovaPreferences(); private CordovaPreferences prefs = new CordovaPreferences();
private Whitelist internalWhitelist = new Whitelist();
private Whitelist externalWhitelist = new Whitelist();
private ArrayList<PluginEntry> pluginEntries = new ArrayList<PluginEntry>(20); private ArrayList<PluginEntry> pluginEntries = new ArrayList<PluginEntry>(20);
public Whitelist getInternalWhitelist() {
return internalWhitelist;
}
public Whitelist getExternalWhitelist() {
return externalWhitelist;
}
public CordovaPreferences getPreferences() { public CordovaPreferences getPreferences() {
return prefs; return prefs;
} }
@ -59,7 +49,7 @@ public class ConfigXmlParser {
public String getLaunchUrl() { public String getLaunchUrl() {
return launchUrl; return launchUrl;
} }
public void parse(Activity action) { public void parse(Activity action) {
// First checking the class namespace for config.xml // First checking the class namespace for config.xml
int id = action.getResources().getIdentifier("config", "xml", action.getClass().getPackage().getName()); int id = action.getResources().getIdentifier("config", "xml", action.getClass().getPackage().getName());
@ -74,78 +64,20 @@ public class ConfigXmlParser {
parse(action.getResources().getXml(id)); parse(action.getResources().getXml(id));
} }
boolean insideFeature = false;
String service = "", pluginClass = "", paramType = "";
boolean onload = false;
public void parse(XmlResourceParser xml) { public void parse(XmlResourceParser xml) {
int eventType = -1; int eventType = -1;
String service = "", pluginClass = "", paramType = "";
boolean onload = false;
boolean insideFeature = false;
// Add implicitly allowed URLs
internalWhitelist.addWhiteListEntry("file:///*", false);
internalWhitelist.addWhiteListEntry("content:///*", false);
internalWhitelist.addWhiteListEntry("data:*", false);
while (eventType != XmlResourceParser.END_DOCUMENT) { while (eventType != XmlResourceParser.END_DOCUMENT) {
if (eventType == XmlResourceParser.START_TAG) { if (eventType == XmlResourceParser.START_TAG) {
String strNode = xml.getName(); handleStartTag(xml);
if (strNode.equals("feature")) {
//Check for supported feature sets aka. plugins (Accelerometer, Geolocation, etc)
//Set the bit for reading params
insideFeature = true;
service = xml.getAttributeValue(null, "name");
}
else if (insideFeature && strNode.equals("param")) {
paramType = xml.getAttributeValue(null, "name");
if (paramType.equals("service")) // check if it is using the older service param
service = xml.getAttributeValue(null, "value");
else if (paramType.equals("package") || paramType.equals("android-package"))
pluginClass = xml.getAttributeValue(null,"value");
else if (paramType.equals("onload"))
onload = "true".equals(xml.getAttributeValue(null, "value"));
}
else if (strNode.equals("access")) {
String origin = xml.getAttributeValue(null, "origin");
String subdomains = xml.getAttributeValue(null, "subdomains");
boolean external = (xml.getAttributeValue(null, "launch-external") != null);
if (origin != null) {
if (external) {
externalWhitelist.addWhiteListEntry(origin, (subdomains != null) && (subdomains.compareToIgnoreCase("true") == 0));
} else {
if ("*".equals(origin)) {
// Special-case * origin to mean http and https when used for internal
// whitelist. This prevents external urls like sms: and geo: from being
// handled internally.
internalWhitelist.addWhiteListEntry("http://*/*", false);
internalWhitelist.addWhiteListEntry("https://*/*", false);
} else {
internalWhitelist.addWhiteListEntry(origin, (subdomains != null) && (subdomains.compareToIgnoreCase("true") == 0));
}
}
}
}
else if (strNode.equals("preference")) {
String name = xml.getAttributeValue(null, "name").toLowerCase(Locale.ENGLISH);
String value = xml.getAttributeValue(null, "value");
prefs.set(name, value);
}
else if (strNode.equals("content")) {
String src = xml.getAttributeValue(null, "src");
if (src != null) {
setStartUrl(src);
}
}
} }
else if (eventType == XmlResourceParser.END_TAG) else if (eventType == XmlResourceParser.END_TAG)
{ {
String strNode = xml.getName(); handleEndTag(xml);
if (strNode.equals("feature")) {
pluginEntries.add(new PluginEntry(service, pluginClass, onload));
service = "";
pluginClass = "";
insideFeature = false;
onload = false;
}
} }
try { try {
eventType = xml.next(); eventType = xml.next();
@ -157,6 +89,48 @@ public class ConfigXmlParser {
} }
} }
public void handleStartTag(XmlResourceParser xml) {
String strNode = xml.getName();
if (strNode.equals("feature")) {
//Check for supported feature sets aka. plugins (Accelerometer, Geolocation, etc)
//Set the bit for reading params
insideFeature = true;
service = xml.getAttributeValue(null, "name");
}
else if (insideFeature && strNode.equals("param")) {
paramType = xml.getAttributeValue(null, "name");
if (paramType.equals("service")) // check if it is using the older service param
service = xml.getAttributeValue(null, "value");
else if (paramType.equals("package") || paramType.equals("android-package"))
pluginClass = xml.getAttributeValue(null,"value");
else if (paramType.equals("onload"))
onload = "true".equals(xml.getAttributeValue(null, "value"));
}
else if (strNode.equals("preference")) {
String name = xml.getAttributeValue(null, "name").toLowerCase(Locale.ENGLISH);
String value = xml.getAttributeValue(null, "value");
prefs.set(name, value);
}
else if (strNode.equals("content")) {
String src = xml.getAttributeValue(null, "src");
if (src != null) {
setStartUrl(src);
}
}
}
public void handleEndTag(XmlResourceParser xml) {
String strNode = xml.getName();
if (strNode.equals("feature")) {
pluginEntries.add(new PluginEntry(service, pluginClass, onload));
service = "";
pluginClass = "";
insideFeature = false;
onload = false;
}
}
private void setStartUrl(String src) { private void setStartUrl(String src) {
Pattern schemeRegex = Pattern.compile("^[a-z-]+://"); Pattern schemeRegex = Pattern.compile("^[a-z-]+://");
Matcher matcher = schemeRegex.matcher(src); Matcher matcher = schemeRegex.matcher(src);

View File

@ -123,8 +123,6 @@ public class CordovaActivity extends Activity implements CordovaInterface {
// Read from config.xml: // Read from config.xml:
protected CordovaPreferences preferences; protected CordovaPreferences preferences;
protected Whitelist internalWhitelist;
protected Whitelist externalWhitelist;
protected String launchUrl; protected String launchUrl;
protected ArrayList<PluginEntry> pluginEntries; protected ArrayList<PluginEntry> pluginEntries;
@ -185,8 +183,6 @@ public class CordovaActivity extends Activity implements CordovaInterface {
preferences = parser.getPreferences(); preferences = parser.getPreferences();
preferences.setPreferencesBundle(getIntent().getExtras()); preferences.setPreferencesBundle(getIntent().getExtras());
preferences.copyIntoIntentExtras(this); preferences.copyIntoIntentExtras(this);
internalWhitelist = parser.getInternalWhitelist();
externalWhitelist = parser.getExternalWhitelist();
launchUrl = parser.getLaunchUrl(); launchUrl = parser.getLaunchUrl();
pluginEntries = parser.getPluginEntries(); pluginEntries = parser.getPluginEntries();
Config.parser = parser; Config.parser = parser;
@ -266,7 +262,7 @@ public class CordovaActivity extends Activity implements CordovaInterface {
// If all else fails, return a default WebView // If all else fails, return a default WebView
ret = new AndroidWebView(this); ret = new AndroidWebView(this);
} }
ret.init(this, pluginEntries, internalWhitelist, externalWhitelist, preferences); ret.init(this, pluginEntries, preferences);
return ret; return ret;
} }
@ -554,7 +550,11 @@ public class CordovaActivity extends Activity implements CordovaInterface {
// If errorUrl specified, then load it // If errorUrl specified, then load it
final String errorUrl = preferences.getString("errorUrl", null); final String errorUrl = preferences.getString("errorUrl", null);
if ((errorUrl != null) && (errorUrl.startsWith("file://") || internalWhitelist.isUrlWhiteListed(errorUrl)) && (!failingUrl.equals(errorUrl))) { CordovaUriHelper helper = new CordovaUriHelper(this, appView);
if ((errorUrl != null) &&
(!failingUrl.equals(errorUrl)) &&
(appView != null && helper.shouldAllowNavigation(errorUrl))
) {
// Load URL on UI thread // Load URL on UI thread
me.runOnUiThread(new Runnable() { me.runOnUiThread(new Runnable() {
public void run() { public void run() {

View File

@ -37,12 +37,14 @@ public class CordovaBridge {
private NativeToJsMessageQueue jsMessageQueue; private NativeToJsMessageQueue jsMessageQueue;
private volatile int expectedBridgeSecret = -1; // written by UI thread, read by JS thread. private volatile int expectedBridgeSecret = -1; // written by UI thread, read by JS thread.
private String loadedUrl; private String loadedUrl;
protected CordovaUriHelper helper;
public CordovaBridge(PluginManager pluginManager, NativeToJsMessageQueue jsMessageQueue) { public CordovaBridge(PluginManager pluginManager, NativeToJsMessageQueue jsMessageQueue, CordovaUriHelper helper) {
this.pluginManager = pluginManager; this.pluginManager = pluginManager;
this.jsMessageQueue = jsMessageQueue; this.jsMessageQueue = jsMessageQueue;
this.helper = helper;
} }
public String jsExec(int bridgeSecret, String service, String action, String callbackId, String arguments) throws JSONException, IllegalAccessException { public String jsExec(int bridgeSecret, String service, String action, String callbackId, String arguments) throws JSONException, IllegalAccessException {
if (!verifySecret("exec()", bridgeSecret)) { if (!verifySecret("exec()", bridgeSecret)) {
return null; return null;
@ -163,9 +165,9 @@ public class CordovaBridge {
} }
else if (defaultValue != null && defaultValue.startsWith("gap_init:")) { else if (defaultValue != null && defaultValue.startsWith("gap_init:")) {
// Protect against random iframes being able to talk through the bridge. // Protect against random iframes being able to talk through the bridge.
// Trust only file URLs and the start URL's domain. // Trust only file URLs and pages which the app would have been allowed
// The extra origin.startsWith("http") is to protect against iframes with data: having "" as origin. // to navigate to anyway.
if (origin.startsWith("file:") || (origin.startsWith("http") && loadedUrl.startsWith(origin))) { if (origin.startsWith("file:") || helper.shouldAllowNavigation(origin)) {
// Enable the bridge // Enable the bridge
int bridgeMode = Integer.parseInt(defaultValue.substring(9)); int bridgeMode = Integer.parseInt(defaultValue.substring(9));
jsMessageQueue.setBridgeMode(bridgeMode); jsMessageQueue.setBridgeMode(bridgeMode);

View File

@ -162,19 +162,67 @@ public class CordovaPlugin {
* Called when an activity you launched exits, giving you the requestCode you started it with, * Called when an activity you launched exits, giving you the requestCode you started it with,
* the resultCode it returned, and any additional data from it. * the resultCode it returned, and any additional data from it.
* *
* @param requestCode The request code originally supplied to startActivityForResult(), * @param requestCode The request code originally supplied to startActivityForResult(),
* allowing you to identify who this result came from. * allowing you to identify who this result came from.
* @param resultCode The integer result code returned by the child activity through its setResult(). * @param resultCode The integer result code returned by the child activity through its setResult().
* @param intent An Intent, which can return result data to the caller (various data can be attached to Intent "extras"). * @param intent An Intent, which can return result data to the caller (various data can be
* attached to Intent "extras").
*/ */
public void onActivityResult(int requestCode, int resultCode, Intent intent) { public void onActivityResult(int requestCode, int resultCode, Intent intent) {
} }
/**
* Hook for blocking the loading of external resources.
*
* This will be called when the WebView's shouldInterceptRequest wants to
* know whether to open a connection to an external resource. Return false
* to block the request: if any plugin returns false, Cordova will block
* the request. If all plugins return null, the default policy will be
* enforced. If at least one plugin returns true, and no plugins return
* false, then the request will proceed.
*
* Note that this only affects resource requests which are routed through
* WebViewClient.shouldInterceptRequest, such as XMLHttpRequest requests and
* img tag loads. WebSockets and media requests (such as <video> and <audio>
* tags) are not affected by this method. Use CSP headers to control access
* to such resources.
*/
public Boolean shouldAllowRequest(String url) {
return null;
}
/**
* Hook for blocking navigation by the Cordova WebView.
*
* This will be called when the WebView's needs to know whether to navigate
* to a new page. Return false to block the navigation: if any plugin
* returns false, Cordova will block the navigation. If all plugins return
* null, the default policy will be enforced. It at least one plugin returns
* true, and no plugins return false, then the navigation will proceed.
*/
public Boolean shouldAllowNavigation(String url) {
return null;
}
/**
* Hook for blocking the launching of Intents by the Cordova application.
*
* This will be called when the WebView will not navigate to a page, but
* could launch an intent to handle the URL. Return false to block this: if
* any plugin returns false, Cordova will block the navigation. If all
* plugins return null, the default policy will be enforced. If at least one
* plugin returns true, and no plugins return false, then the URL will be
* opened.
*/
public Boolean shouldOpenExternalUrl(String url) {
return null;
}
/** /**
* By specifying a <url-filter> in config.xml you can map a URL (using startsWith atm) to this method. * By specifying a <url-filter> in config.xml you can map a URL (using startsWith atm) to this method.
* *
* @param url The URL that is trying to be loaded in the Cordova webview. * @param url The URL that is trying to be loaded in the Cordova webview.
* @return Return true to prevent the URL from loading. Default is false. * @return Return true to prevent the URL from loading. Default is false.
*/ */
public boolean onOverrideUrlLoading(String url) { public boolean onOverrideUrlLoading(String url) {
return false; return false;

View File

@ -23,6 +23,7 @@ import android.annotation.TargetApi;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.util.Log;
import android.webkit.WebView; import android.webkit.WebView;
public class CordovaUriHelper { public class CordovaUriHelper {
@ -37,11 +38,77 @@ public class CordovaUriHelper {
appView = webView; appView = webView;
cordova = cdv; cordova = cdv;
} }
/**
* Determine whether the webview should be allowed to navigate to a given URL.
*
* This method implements the default whitelist policy when no plugins override
* shouldAllowNavigation
*/
public boolean shouldAllowNavigation(String url) {
Boolean pluginManagerAllowsNavigation = this.appView.getPluginManager().shouldAllowNavigation(url);
if (pluginManagerAllowsNavigation == null) {
// Default policy:
// Internal urls on file:// or data:// that do not contain "/app_webview/" are allowed for navigation
if(url.startsWith("file://") || url.startsWith("data:"))
{
//This directory on WebKit/Blink based webviews contains SQLite databases!
//DON'T CHANGE THIS UNLESS YOU KNOW WHAT YOU'RE DOING!
return !url.contains("/app_webview/");
}
return false;
}
return pluginManagerAllowsNavigation;
}
/**
* Determine whether the webview should be allowed to launch an intent for a given URL.
*
* This method implements the default whitelist policy when no plugins override
* shouldOpenExternalUrl
*/
public boolean shouldOpenExternalUrl(String url) {
Boolean pluginManagerAllowsExternalUrl = this.appView.getPluginManager().shouldOpenExternalUrl(url);
if (pluginManagerAllowsExternalUrl == null) {
// Default policy:
// External URLs are not allowed
return false;
}
return pluginManagerAllowsExternalUrl;
}
/**
* Determine whether the webview should be allowed to request a resource from a given URL.
*
* This method implements the default whitelist policy when no plugins override
* shouldAllowRequest
*/
public boolean shouldAllowRequest(String url) {
Boolean pluginManagerAllowsRequest = this.appView.getPluginManager().shouldAllowRequest(url);
if (pluginManagerAllowsRequest == null) {
// Default policy:
// Internal urls on file:// or data:// that do not contain "/app_webview/" are allowed for navigation
if(url.startsWith("file://") || url.startsWith("data:"))
{
//This directory on WebKit/Blink based webviews contains SQLite databases!
//DON'T CHANGE THIS UNLESS YOU KNOW WHAT YOU'RE DOING!
return !url.contains("/app_webview/");
}
return false;
}
return pluginManagerAllowsRequest;
}
/** /**
* Give the host application a chance to take over the control when a new url * Give the host application a chance to take over the control when a new url
* is about to be loaded in the current WebView. * is about to be loaded in the current WebView.
* *
* This method implements the default whitelist policy when no plugins override
* the whitelist methods:
* Internal urls on file:// or data:// that do not contain "app_webview" are allowed for navigation
* External urls are not allowed.
*
* @param view The WebView that is initiating the callback. * @param view The WebView that is initiating the callback.
* @param url The url to be loaded. * @param url The url to be loaded.
* @return true to override, false for default behavior * @return true to override, false for default behavior
@ -49,23 +116,15 @@ public class CordovaUriHelper {
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
public boolean shouldOverrideUrlLoading(String url) { public boolean shouldOverrideUrlLoading(String url) {
// Give plugins the chance to handle the url // Give plugins the chance to handle the url
if (this.appView.getPluginManager().onOverrideUrlLoading(url)) { if (shouldAllowNavigation(url)) {
// Do nothing other than what the plugins wanted.
// If any returned true, then the request was handled.
return true;
}
else if(url.startsWith("file://") | url.startsWith("data:"))
{
//This directory on WebKit/Blink based webviews contains SQLite databases!
//DON'T CHANGE THIS UNLESS YOU KNOW WHAT YOU'RE DOING!
return url.contains("app_webview");
}
else if (appView.getWhitelist().isUrlWhiteListed(url)) {
// Allow internal navigation // Allow internal navigation
return false; return false;
} }
else if (appView.getExternalWhitelist().isUrlWhiteListed(url)) if (shouldOpenExternalUrl(url)) {
{ // Do nothing other than what the plugins wanted.
// If any returned false, then the request was either blocked
// completely, or handled out-of-band by the plugin. If they all
// returned true, then we should open the URL here.
try { try {
Intent intent = new Intent(Intent.ACTION_VIEW); Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(url)); intent.setData(Uri.parse(url));
@ -77,10 +136,11 @@ public class CordovaUriHelper {
this.cordova.getActivity().startActivity(intent); this.cordova.getActivity().startActivity(intent);
return true; return true;
} catch (android.content.ActivityNotFoundException e) { } catch (android.content.ActivityNotFoundException e) {
LOG.e(TAG, "Error loading url " + url, e); Log.e(TAG, "Error loading url " + url, e);
} }
return true;
} }
// Intercept the request and do nothing with it -- block it // Block by default
return true; return true;
} }
} }

View File

@ -13,7 +13,6 @@ public interface CordovaWebView {
public static final String CORDOVA_VERSION = "4.0.0-dev"; public static final String CORDOVA_VERSION = "4.0.0-dev";
void init(CordovaInterface cordova, List<PluginEntry> pluginEntries, void init(CordovaInterface cordova, List<PluginEntry> pluginEntries,
Whitelist internalWhitelist, Whitelist externalWhitelist,
CordovaPreferences preferences); CordovaPreferences preferences);
View getView(); View getView();
@ -81,8 +80,6 @@ public interface CordovaWebView {
PluginManager getPluginManager(); PluginManager getPluginManager();
Whitelist getWhitelist();
Whitelist getExternalWhitelist();
CordovaPreferences getPreferences(); CordovaPreferences getPreferences();
void onFilePickerResult(Uri uri); void onFilePickerResult(Uri uri);

View File

@ -45,7 +45,7 @@ public class IceCreamCordovaWebViewClient extends AndroidWebViewClient {
try { try {
// Check the against the whitelist and lock out access to the WebView directory // Check the against the whitelist and lock out access to the WebView directory
// Changing this will cause problems for your application // Changing this will cause problems for your application
if (isUrlHarmful(url)) { if (!helper.shouldAllowRequest(url)) {
LOG.w(TAG, "URL blocked by whitelist: " + url); LOG.w(TAG, "URL blocked by whitelist: " + url);
// Results in a 404. // Results in a 404.
return new WebResourceResponse("text/plain", "UTF-8", null); return new WebResourceResponse("text/plain", "UTF-8", null);
@ -71,11 +71,6 @@ public class IceCreamCordovaWebViewClient extends AndroidWebViewClient {
} }
} }
private boolean isUrlHarmful(String url) {
return ((url.startsWith("http:") || url.startsWith("https:")) && !appView.getWhitelist().isUrlWhiteListed(url))
|| url.contains("app_webview");
}
private static boolean needsKitKatContentUrlFix(Uri uri) { private static boolean needsKitKatContentUrlFix(Uri uri) {
return android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT && "content".equals(uri.getScheme()); return android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT && "content".equals(uri.getScheme());
} }

View File

@ -253,6 +253,110 @@ public class PluginManager {
} }
} }
/**
* Called when the webview is going to request an external resource.
*
* This delegates to the installed plugins, which must all return true for
* this method to return true.
*
* @param url The URL that is being requested.
* @return Tri-State:
* null: All plugins returned null (the default). This
* indicates that the default policy should be
* followed.
* true: All plugins returned true (allow the resource
* to load)
* false: At least one plugin returned false (block the
* resource)
*/
public Boolean shouldAllowRequest(String url) {
Boolean anyResponded = null;
for (PluginEntry entry : this.entryMap.values()) {
CordovaPlugin plugin = pluginMap.get(entry.service);
if (plugin != null) {
Boolean result = plugin.shouldAllowRequest(url);
if (result != null) {
anyResponded = true;
if (!result) {
return false;
}
}
}
}
// This will be true if all plugins allow the request, or null if no plugins override the method
return anyResponded;
}
/**
* Called when the webview is going to change the URL of the loaded content.
*
* This delegates to the installed plugins, which must all return true for
* this method to return true. A true result will allow the new page to load;
* a false result will prevent the page from loading.
*
* @param url The URL that is being requested.
* @return Tri-State:
* null: All plugins returned null (the default). This
* indicates that the default policy should be
* followed.
* true: All plugins returned true (allow the navigation)
* false: At least one plugin returned false (block the
* navigation)
*/
public Boolean shouldAllowNavigation(String url) {
Boolean anyResponded = null;
for (PluginEntry entry : this.entryMap.values()) {
CordovaPlugin plugin = pluginMap.get(entry.service);
if (plugin != null) {
Boolean result = plugin.shouldAllowNavigation(url);
if (result != null) {
anyResponded = true;
if (!result) {
return false;
}
}
}
}
// This will be true if all plugins allow the request, or null if no plugins override the method
return anyResponded;
}
/**
* Called when the webview is going not going to navigate, but may launch
* an Intent for an URL.
*
* This delegates to the installed plugins, which must all return true for
* this method to return true. A true result will allow the URL to launch;
* a false result will prevent the URL from loading.
*
* @param url The URL that is being requested.
* @return Tri-State:
* null: All plugins returned null (the default). This
* indicates that the default policy should be
* followed.
* true: All plugins returned true (allow the URL to
* launch an intent)
* false: At least one plugin returned false (block the
* intent)
*/
public Boolean shouldOpenExternalUrl(String url) {
Boolean anyResponded = null;
for (PluginEntry entry : this.entryMap.values()) {
CordovaPlugin plugin = pluginMap.get(entry.service);
if (plugin != null) {
Boolean result = plugin.shouldOpenExternalUrl(url);
if (result != null) {
anyResponded = true;
if (!result) {
return false;
}
}
}
}
// This will be true if all plugins allow the request, or null if no plugins override the method
return anyResponded;
}
/** /**
* Called when the URL of the webview changes. * Called when the URL of the webview changes.
* *

View File

@ -51,8 +51,7 @@ public class CordovaWebViewTestActivity extends Activity implements CordovaInter
Config.init(this); Config.init(this);
cordovaWebView = (CordovaWebView) findViewById(R.id.cordovaWebView); cordovaWebView = (CordovaWebView) findViewById(R.id.cordovaWebView);
cordovaWebView.init(this, Config.getPluginEntries(), Config.getWhitelist(), cordovaWebView.init(this, Config.getPluginEntries(), Config.getPreferences());
Config.getExternalWhitelist(), Config.getPreferences());
cordovaWebView.loadUrl("file:///android_asset/www/index.html"); cordovaWebView.loadUrl("file:///android_asset/www/index.html");

View File

@ -34,7 +34,7 @@ public class menus extends CordovaActivity {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
// need the title to be shown (config.xml) for the options menu to be visible // need the title to be shown (config.xml) for the options menu to be visible
super.init(); super.init();
super.registerForContextMenu(super.appView); super.registerForContextMenu(super.appView.getView());
super.loadUrl("file:///android_asset/www/menus/index.html"); super.loadUrl("file:///android_asset/www/menus/index.html");
} }