diff --git a/framework/src/org/apache/cordova/AndroidChromeClient.java b/framework/src/org/apache/cordova/AndroidChromeClient.java index e0e9dfab..31d9b683 100755 --- a/framework/src/org/apache/cordova/AndroidChromeClient.java +++ b/framework/src/org/apache/cordova/AndroidChromeClient.java @@ -44,6 +44,9 @@ import android.widget.EditText; import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.RelativeLayout; +import android.util.Log; + + /** * This class is the WebChromeClient that implements callbacks for our web view. @@ -60,7 +63,6 @@ public class AndroidChromeClient extends WebChromeClient implements CordovaChrom public static final int FILECHOOSER_RESULTCODE = 5173; private static final String LOG_TAG = "CordovaChromeClient"; - private String TAG = "CordovaLog"; private long MAX_QUOTA = 100 * 1024 * 1024; protected CordovaInterface cordova; protected CordovaWebView appView; @@ -201,68 +203,75 @@ public class AndroidChromeClient extends WebChromeClient implements CordovaChrom * Since we are hacking prompts for our own purposes, we should not be using them for * this purpose, perhaps we should hack console.log to do this instead! * - * @param view - * @param url - * @param message - * @param defaultValue - * @param result * @see Other implementation in the Dialogs plugin. */ @Override - public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { - - // Security check to make sure any requests are coming from the page initially - // loaded in webview and not another loaded in an iframe. - boolean reqOk = false; - if (url.startsWith("file://") || Config.isUrlWhiteListed(url)) { - reqOk = true; - } - - // Calling PluginManager.exec() to call a native service using - // prompt(this.stringify(args), "gap:"+this.stringify([service, action, callbackId, true])); - if (reqOk && defaultValue != null && defaultValue.length() > 3 && defaultValue.substring(0, 4).equals("gap:")) { + public boolean onJsPrompt(WebView view, String origin, String message, String defaultValue, JsPromptResult result) { + // Unlike the @JavascriptInterface bridge, this method is always called on the UI thread. + if (defaultValue != null && defaultValue.length() > 3 && defaultValue.startsWith("gap:")) { JSONArray array; try { array = new JSONArray(defaultValue.substring(4)); - String service = array.getString(0); - String action = array.getString(1); - String callbackId = array.getString(2); - - //String r = this.appView.exposedJsApi.exec(service, action, callbackId, message); - String r = this.appView.exec(service, action, callbackId, message); + int bridgeSecret = array.getInt(0); + String service = array.getString(1); + String action = array.getString(2); + String callbackId = array.getString(3); + String r = appView.exposedJsApi.exec(bridgeSecret, service, action, callbackId, message); result.confirm(r == null ? "" : r); } catch (JSONException e) { e.printStackTrace(); - return false; + result.cancel(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + result.cancel(); } } // Sets the native->JS bridge mode. - else if (reqOk && defaultValue != null && defaultValue.equals("gap_bridge_mode:")) { - try { - //this.appView.exposedJsApi.setNativeToJsBridgeMode(Integer.parseInt(message)); - this.appView.setNativeToJsBridgeMode(Integer.parseInt(message)); - result.confirm(""); - } catch (NumberFormatException e){ - result.confirm(""); + else if (defaultValue != null && defaultValue.startsWith("gap_bridge_mode:")) { + try { + int bridgeSecret = Integer.parseInt(defaultValue.substring(16)); + appView.exposedJsApi.setNativeToJsBridgeMode(bridgeSecret, Integer.parseInt(message)); + result.cancel(); + } catch (NumberFormatException e){ e.printStackTrace(); - } + result.cancel(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + result.cancel(); + } } // Polling for JavaScript messages - else if (reqOk && defaultValue != null && defaultValue.equals("gap_poll:")) { - //String r = this.appView.exposedJsApi.retrieveJsMessages("1".equals(message)); - String r = this.appView.retrieveJsMessages("1".equals(message)); - result.confirm(r == null ? "" : r); + else if (defaultValue != null && defaultValue.startsWith("gap_poll:")) { + int bridgeSecret = Integer.parseInt(defaultValue.substring(9)); + try { + String r = appView.exposedJsApi.retrieveJsMessages(bridgeSecret, "1".equals(message)); + result.confirm(r == null ? "" : r); + } catch (IllegalAccessException e) { + e.printStackTrace(); + result.cancel(); + } } - // Do NO-OP so older code doesn't display dialog - else if (defaultValue != null && defaultValue.equals("gap_init:")) { - result.confirm("OK"); - } - - // Show dialog - else { + else if (defaultValue != null && defaultValue.startsWith("gap_init:")) { + String startUrl = Config.getStartUrl(); + // Protect against random iframes being able to talk through the bridge. + // Trust only file URLs and the start URL's domain. + // The extra origin.startsWith("http") is to protect against iframes with data: having "" as origin. + if (origin.startsWith("file:") || (origin.startsWith("http") && startUrl.startsWith(origin))) { + // Enable the bridge + int bridgeMode = Integer.parseInt(defaultValue.substring(9)); + appView.jsMessageQueue.setBridgeMode(bridgeMode); + // Tell JS the bridge secret. + int secret = appView.exposedJsApi.generateBridgeSecret(); + result.confirm(""+secret); + } else { + Log.e(LOG_TAG, "gap_init called from restricted origin: " + origin); + result.cancel(); + } + } else { + // Returning false would also show a dialog, but the default one shows the origin (ugly). final JsPromptResult res = result; AlertDialog.Builder dlg = new AlertDialog.Builder(this.cordova.getActivity()); dlg.setMessage(message); @@ -297,7 +306,7 @@ public class AndroidChromeClient extends WebChromeClient implements CordovaChrom public void onExceededDatabaseQuota(String url, String databaseIdentifier, long currentQuota, long estimatedSize, long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) { - LOG.d(TAG, "onExceededDatabaseQuota estimatedSize: %d currentQuota: %d totalUsedQuota: %d", estimatedSize, currentQuota, totalUsedQuota); + LOG.d(LOG_TAG, "onExceededDatabaseQuota estimatedSize: %d currentQuota: %d totalUsedQuota: %d", estimatedSize, currentQuota, totalUsedQuota); quotaUpdater.updateQuota(MAX_QUOTA); } @@ -310,7 +319,7 @@ public class AndroidChromeClient extends WebChromeClient implements CordovaChrom //This is only for Android 2.1 if(android.os.Build.VERSION.SDK_INT == android.os.Build.VERSION_CODES.ECLAIR_MR1) { - LOG.d(TAG, "%s: Line %d : %s", sourceID, lineNumber, message); + LOG.d(LOG_TAG, "%s: Line %d : %s", sourceID, lineNumber, message); super.onConsoleMessage(message, lineNumber, sourceID); } } @@ -320,7 +329,7 @@ public class AndroidChromeClient extends WebChromeClient implements CordovaChrom public boolean onConsoleMessage(ConsoleMessage consoleMessage) { if (consoleMessage.message() != null) - LOG.d(TAG, "%s: Line %d : %s" , consoleMessage.sourceId() , consoleMessage.lineNumber(), consoleMessage.message()); + LOG.d(LOG_TAG, "%s: Line %d : %s" , consoleMessage.sourceId() , consoleMessage.lineNumber(), consoleMessage.message()); return super.onConsoleMessage(consoleMessage); } @@ -342,10 +351,10 @@ public class AndroidChromeClient extends WebChromeClient implements CordovaChrom this.appView.showCustomView(view, callback); } - @Override - public void onHideCustomView() { - this.appView.hideCustomView(); - } + @Override + public void onHideCustomView() { + this.appView.hideCustomView(); + } @Override /** @@ -355,31 +364,31 @@ public class AndroidChromeClient extends WebChromeClient implements CordovaChrom */ public View getVideoLoadingProgressView() { - if (mVideoProgressView == null) { - // Create a new Loading view programmatically. - - // create the linear layout - LinearLayout layout = new LinearLayout(this.appView.getContext()); - layout.setOrientation(LinearLayout.VERTICAL); - RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); - layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT); - layout.setLayoutParams(layoutParams); - // the proress bar - ProgressBar bar = new ProgressBar(this.appView.getContext()); - LinearLayout.LayoutParams barLayoutParams = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); - barLayoutParams.gravity = Gravity.CENTER; - bar.setLayoutParams(barLayoutParams); - layout.addView(bar); - - mVideoProgressView = layout; - } + if (mVideoProgressView == null) { + // Create a new Loading view programmatically. + + // create the linear layout + LinearLayout layout = new LinearLayout(this.appView.getContext()); + layout.setOrientation(LinearLayout.VERTICAL); + RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT); + layout.setLayoutParams(layoutParams); + // the proress bar + ProgressBar bar = new ProgressBar(this.appView.getContext()); + LinearLayout.LayoutParams barLayoutParams = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + barLayoutParams.gravity = Gravity.CENTER; + bar.setLayoutParams(barLayoutParams); + layout.addView(bar); + + mVideoProgressView = layout; + } return mVideoProgressView; } public void openFileChooser(ValueCallback uploadMsg) { this.openFileChooser(uploadMsg, "*/*"); } - + public void openFileChooser( ValueCallback uploadMsg, String acceptType ) { this.openFileChooser(uploadMsg, acceptType, null); } diff --git a/framework/src/org/apache/cordova/AndroidExposedJsApi.java b/framework/src/org/apache/cordova/AndroidExposedJsApi.java index 74945ccd..5f53774e 100755 --- a/framework/src/org/apache/cordova/AndroidExposedJsApi.java +++ b/framework/src/org/apache/cordova/AndroidExposedJsApi.java @@ -27,18 +27,20 @@ import org.json.JSONException; * an equivalent entry in CordovaChromeClient.java, and be added to * cordova-js/lib/android/plugin/android/promptbasednativeapi.js */ -public /* package */ class AndroidExposedJsApi implements ExposedJsApi { - +class AndroidExposedJsApi implements ExposedJsApi { + private PluginManager pluginManager; private NativeToJsMessageQueue jsMessageQueue; - + private volatile int bridgeSecret = -1; // written by UI thread, read by JS thread. + public AndroidExposedJsApi(PluginManager pluginManager, NativeToJsMessageQueue jsMessageQueue) { this.pluginManager = pluginManager; this.jsMessageQueue = jsMessageQueue; } @JavascriptInterface - public String exec(String service, String action, String callbackId, String arguments) throws JSONException { + public String exec(int bridgeSecret, String service, String action, String callbackId, String arguments) throws JSONException, IllegalAccessException { + verifySecret(bridgeSecret); // If the arguments weren't received, send a message back to JS. It will switch bridge modes and try again. See CB-2666. // We send a message meant specifically for this case. It starts with "@" so no other message can be encoded into the same string. if (arguments == null) { @@ -49,7 +51,7 @@ public /* package */ class AndroidExposedJsApi implements ExposedJsApi { try { // Tell the resourceApi what thread the JS is running on. CordovaResourceApi.jsThread = Thread.currentThread(); - + pluginManager.exec(service, action, callbackId, arguments); String ret = ""; if (!NativeToJsMessageQueue.DISABLE_EXEC_CHAINING) { @@ -63,14 +65,33 @@ public /* package */ class AndroidExposedJsApi implements ExposedJsApi { jsMessageQueue.setPaused(false); } } - + @JavascriptInterface - public void setNativeToJsBridgeMode(int value) { + public void setNativeToJsBridgeMode(int bridgeSecret, int value) throws IllegalAccessException { + verifySecret(bridgeSecret); jsMessageQueue.setBridgeMode(value); } - + @JavascriptInterface - public String retrieveJsMessages(boolean fromOnlineEvent) { + public String retrieveJsMessages(int bridgeSecret, boolean fromOnlineEvent) throws IllegalAccessException { + verifySecret(bridgeSecret); return jsMessageQueue.popAndEncode(fromOnlineEvent); } + + private void verifySecret(int value) throws IllegalAccessException { + if (bridgeSecret < 0 || value != bridgeSecret) { + throw new IllegalAccessException(); + } + } + + /** Called on page transitions */ + void clearBridgeSecret() { + bridgeSecret = -1; + } + + /** Called by cordova.js to initialize the bridge. */ + int generateBridgeSecret() { + bridgeSecret = (int)(Math.random() * Integer.MAX_VALUE); + return bridgeSecret; + } } diff --git a/framework/src/org/apache/cordova/AndroidWebView.java b/framework/src/org/apache/cordova/AndroidWebView.java index 5fd64953..a169d307 100755 --- a/framework/src/org/apache/cordova/AndroidWebView.java +++ b/framework/src/org/apache/cordova/AndroidWebView.java @@ -412,17 +412,7 @@ public class AndroidWebView extends WebView implements CordovaWebView { this.loadUrlNow(url); } else { - - String initUrl = this.getProperty("url", null); - - // If first page of app, then set URL to load to be the one passed in - if (initUrl == null) { - this.loadUrlIntoView(url); - } - // Otherwise use the URL specified in the activity's extras bundle - else { - this.loadUrlIntoView(initUrl); - } + this.loadUrlIntoView(url); } } @@ -433,16 +423,15 @@ public class AndroidWebView extends WebView implements CordovaWebView { * @param url * @param time The number of ms to wait before loading webview */ + @Deprecated public void loadUrl(final String url, int time) { - String initUrl = this.getProperty("url", null); - - // If first page of app, then set URL to load to be the one passed in - if (initUrl == null) { - this.loadUrlIntoView(url, time); + if(url == null) + { + this.loadUrlIntoView(Config.getStartUrl()); } - // Otherwise use the URL specified in the activity's extras bundle - else { - this.loadUrlIntoView(initUrl); + else + { + this.loadUrlIntoView(url); } } @@ -1027,6 +1016,7 @@ public class AndroidWebView extends WebView implements CordovaWebView { boundKeyCodes.clear(); pluginManager.onReset(); jsMessageQueue.reset(); + exposedJsApi.clearBridgeSecret(); } @Override diff --git a/framework/src/org/apache/cordova/AndroidWebViewClient.java b/framework/src/org/apache/cordova/AndroidWebViewClient.java index b57e0b66..f3b295bf 100755 --- a/framework/src/org/apache/cordova/AndroidWebViewClient.java +++ b/framework/src/org/apache/cordova/AndroidWebViewClient.java @@ -21,8 +21,6 @@ package org.apache.cordova; import java.io.ByteArrayInputStream; import java.util.Hashtable; -import org.apache.cordova.CordovaInterface; -import org.apache.cordova.LOG; import org.json.JSONException; import org.json.JSONObject; @@ -32,16 +30,15 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.graphics.Bitmap; -import android.net.Uri; import android.net.http.SslError; import android.util.Log; import android.view.View; import android.webkit.HttpAuthHandler; import android.webkit.SslErrorHandler; -import android.webkit.WebResourceResponse; import android.webkit.WebView; import android.webkit.WebViewClient; + /** * This class is the WebViewClient that implements callbacks for our web view. * The kind of callbacks that happen here are regarding the rendering of the @@ -56,8 +53,8 @@ import android.webkit.WebViewClient; */ public class AndroidWebViewClient extends WebViewClient implements CordovaWebViewClient{ - private static final String TAG = "AndroidWebViewClient"; - private static final String CORDOVA_EXEC_URL_PREFIX = "http://cdv_exec/"; + private static final String TAG = "AndroidWebViewClient"; + private static final String CORDOVA_EXEC_URL_PREFIX = "http://cdv_exec/"; CordovaInterface cordova; AndroidWebView appView; CordovaUriHelper helper; @@ -98,30 +95,6 @@ public class AndroidWebViewClient extends WebViewClient implements CordovaWebVie helper = new CordovaUriHelper(cordova, view); } - - // Parses commands sent by setting the webView's URL to: - // cdvbrg:service/action/callbackId#jsonArgs - private void handleExecUrl(String url) { - int idx1 = CORDOVA_EXEC_URL_PREFIX.length(); - int idx2 = url.indexOf('#', idx1 + 1); - int idx3 = url.indexOf('#', idx2 + 1); - int idx4 = url.indexOf('#', idx3 + 1); - if (idx1 == -1 || idx2 == -1 || idx3 == -1 || idx4 == -1) { - Log.e(TAG, "Could not decode URL command: " + url); - return; - } - String service = url.substring(idx1, idx2); - String action = url.substring(idx2 + 1, idx3); - String callbackId = url.substring(idx3 + 1, idx4); - String jsonArgs = url.substring(idx4 + 1); - try { - appView.exec(service, action, callbackId, jsonArgs); - } catch (JSONException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - /** * Give the host application a chance to take over the control when a new url * is about to be loaded in the current WebView. @@ -132,115 +105,7 @@ public class AndroidWebViewClient extends WebViewClient implements CordovaWebVie */ @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { - // Check if it's an exec() bridge command message. - /* - if (NativeToJsMessageQueue.ENABLE_LOCATION_CHANGE_EXEC_MODE && url.startsWith(CORDOVA_EXEC_URL_PREFIX)) { - handleExecUrl(url); - } - - // Give plugins the chance to handle the url - else if (this.appView.onOverrideUrlLoading(url)) { - } - - // If dialing phone (tel:5551212) - else if (url.startsWith(WebView.SCHEME_TEL)) { - try { - Intent intent = new Intent(Intent.ACTION_DIAL); - intent.setData(Uri.parse(url)); - this.cordova.getActivity().startActivity(intent); - } catch (android.content.ActivityNotFoundException e) { - LOG.e(TAG, "Error dialing " + url + ": " + e.toString()); - } - } - - // If displaying map (geo:0,0?q=address) - else if (url.startsWith("geo:")) { - try { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setData(Uri.parse(url)); - this.cordova.getActivity().startActivity(intent); - } catch (android.content.ActivityNotFoundException e) { - LOG.e(TAG, "Error showing map " + url + ": " + e.toString()); - } - } - - // If sending email (mailto:abc@corp.com) - else if (url.startsWith(WebView.SCHEME_MAILTO)) { - try { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setData(Uri.parse(url)); - this.cordova.getActivity().startActivity(intent); - } catch (android.content.ActivityNotFoundException e) { - LOG.e(TAG, "Error sending email " + url + ": " + e.toString()); - } - } - - // If sms:5551212?body=This is the message - else if (url.startsWith("sms:")) { - try { - Intent intent = new Intent(Intent.ACTION_VIEW); - - // Get address - String address = null; - int parmIndex = url.indexOf('?'); - if (parmIndex == -1) { - address = url.substring(4); - } - else { - address = url.substring(4, parmIndex); - - // If body, then set sms body - Uri uri = Uri.parse(url); - String query = uri.getQuery(); - if (query != null) { - if (query.startsWith("body=")) { - intent.putExtra("sms_body", query.substring(5)); - } - } - } - intent.setData(Uri.parse("sms:" + address)); - intent.putExtra("address", address); - intent.setType("vnd.android-dir/mms-sms"); - this.cordova.getActivity().startActivity(intent); - } catch (android.content.ActivityNotFoundException e) { - LOG.e(TAG, "Error sending sms " + url + ":" + e.toString()); - } - } - - //Android Market - else if(url.startsWith("market:")) { - try { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setData(Uri.parse(url)); - this.cordova.getActivity().startActivity(intent); - } catch (android.content.ActivityNotFoundException e) { - LOG.e(TAG, "Error loading Google Play Store: " + url, e); - } - } - - // All else - else { - - // If our app or file:, then load into a new Cordova webview container by starting a new instance of our activity. - // Our app continues to run. When BACK is pressed, our app is redisplayed. - if (url.startsWith("file://") || url.startsWith("data:") || Config.isUrlWhiteListed(url)) { - return false; - } - - // If not our application, let default viewer handle - else { - try { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setData(Uri.parse(url)); - this.cordova.getActivity().startActivity(intent); - } catch (android.content.ActivityNotFoundException e) { - LOG.e(TAG, "Error loading url " + url, e); - } - } - } - return true; - */ - return helper.shouldOverrideUrlLoading(view, url); + return helper.shouldOverrideUrlLoading(view, url); } /** diff --git a/framework/src/org/apache/cordova/Config.java b/framework/src/org/apache/cordova/Config.java index 31a1370a..5da08566 100644 --- a/framework/src/org/apache/cordova/Config.java +++ b/framework/src/org/apache/cordova/Config.java @@ -20,21 +20,16 @@ package org.apache.cordova; import java.io.IOException; - import java.util.Locale; - import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.cordova.LOG; - import org.xmlpull.v1.XmlPullParserException; import android.app.Activity; - import android.content.res.XmlResourceParser; import android.graphics.Color; - import android.util.Log; public class Config { @@ -44,6 +39,8 @@ public class Config { private Whitelist whitelist = new Whitelist(); private String startUrl; + private static String errorUrl; + private static Config self = null; public static void init(Activity action) { @@ -156,6 +153,10 @@ public class Config { boolean value = xml.getAttributeValue(null, "value").equals("true"); action.getIntent().putExtra(name, value); } + else if(name.equalsIgnoreCase("errorurl")) + { + errorUrl = xml.getAttributeValue(null, "value"); + } else { String value = xml.getAttributeValue(null, "value"); @@ -230,4 +231,8 @@ public class Config { } return self.startUrl; } + + public static String getErrorUrl() { + return errorUrl; + } } diff --git a/framework/src/org/apache/cordova/CordovaActivity.java b/framework/src/org/apache/cordova/CordovaActivity.java index bbe7c36f..c8b9e963 100755 --- a/framework/src/org/apache/cordova/CordovaActivity.java +++ b/framework/src/org/apache/cordova/CordovaActivity.java @@ -608,7 +608,7 @@ public class CordovaActivity extends Activity implements CordovaInterface { //Code to test CB-3064 - String errorUrl = this.getStringProperty("ErrorUrl", null); + String errorUrl = Config.getErrorUrl(); LOG.d(TAG, "CB-3064: The errorUrl is " + errorUrl); if (this.activityState == ACTIVITY_STARTING) { diff --git a/framework/src/org/apache/cordova/CordovaChromeClient.java b/framework/src/org/apache/cordova/CordovaChromeClient.java index 847a4665..87177b4f 100644 --- a/framework/src/org/apache/cordova/CordovaChromeClient.java +++ b/framework/src/org/apache/cordova/CordovaChromeClient.java @@ -22,11 +22,6 @@ import android.net.Uri; import android.webkit.ValueCallback; public interface CordovaChromeClient { - - int FILECHOOSER_RESULTCODE = 0; - void setWebView(CordovaWebView appView); - ValueCallback getValueCallback(); - } diff --git a/framework/src/org/apache/cordova/CordovaUriHelper.java b/framework/src/org/apache/cordova/CordovaUriHelper.java index 5e9aa841..3a0292ed 100644 --- a/framework/src/org/apache/cordova/CordovaUriHelper.java +++ b/framework/src/org/apache/cordova/CordovaUriHelper.java @@ -19,17 +19,13 @@ package org.apache.cordova; -import org.json.JSONException; - import android.content.Intent; import android.net.Uri; -import android.util.Log; import android.webkit.WebView; public class CordovaUriHelper { private static final String TAG = "CordovaUriHelper"; - private static final String CORDOVA_EXEC_URL_PREFIX = "http://cdv_exec/"; private CordovaWebView appView; private CordovaInterface cordova; @@ -40,32 +36,6 @@ public class CordovaUriHelper { cordova = cdv; } - // Parses commands sent by setting the webView's URL to: - // cdvbrg:service/action/callbackId#jsonArgs - void handleExecUrl(String url) { - int idx1 = CORDOVA_EXEC_URL_PREFIX.length(); - int idx2 = url.indexOf('#', idx1 + 1); - int idx3 = url.indexOf('#', idx2 + 1); - int idx4 = url.indexOf('#', idx3 + 1); - if (idx1 == -1 || idx2 == -1 || idx3 == -1 || idx4 == -1) { - Log.e(TAG, "Could not decode URL command: " + url); - return; - } - String service = url.substring(idx1, idx2); - String action = url.substring(idx2 + 1, idx3); - String callbackId = url.substring(idx3 + 1, idx4); - String jsonArgs = url.substring(idx4 + 1); - try { - appView.exec(service, action, callbackId, jsonArgs); - //There is no reason to not send this directly to the pluginManager - - } catch (JSONException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - - /** * Give the host application a chance to take over the control when a new url * is about to be loaded in the current WebView. @@ -76,14 +46,10 @@ public class CordovaUriHelper { */ public boolean shouldOverrideUrlLoading(WebView view, String url) { // The WebView should support http and https when going on the Internet - if(url.startsWith("http")) + if(url.startsWith("http:") || url.startsWith("https:")) { - // Check if it's an exec() bridge command message. - if (NativeToJsMessageQueue.ENABLE_LOCATION_CHANGE_EXEC_MODE && url.startsWith(CORDOVA_EXEC_URL_PREFIX)) { - handleExecUrl(url); - } // We only need to whitelist sites on the Internet! - else if(Config.isUrlWhiteListed(url)) + if(Config.isUrlWhiteListed(url)) { return false; } @@ -94,7 +60,9 @@ public class CordovaUriHelper { } else if(url.startsWith("file://") | url.startsWith("data:")) { - return false; + //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 { @@ -109,5 +77,4 @@ public class CordovaUriHelper { //Default behaviour should be to load the default intent, let's see what happens! return true; } - } diff --git a/framework/src/org/apache/cordova/ExposedJsApi.java b/framework/src/org/apache/cordova/ExposedJsApi.java index d0f8abf5..b84fcfb9 100644 --- a/framework/src/org/apache/cordova/ExposedJsApi.java +++ b/framework/src/org/apache/cordova/ExposedJsApi.java @@ -2,16 +2,11 @@ package org.apache.cordova; import org.json.JSONException; -import android.webkit.JavascriptInterface; - /* * Any exposed Javascript API MUST implement these three things! */ - public interface ExposedJsApi { - - @JavascriptInterface - public String exec(String service, String action, String callbackId, String arguments) throws JSONException; - public void setNativeToJsBridgeMode(int value); - public String retrieveJsMessages(boolean fromOnlineEvent); + public String exec(int bridgeSecret, String service, String action, String callbackId, String arguments) throws JSONException, IllegalAccessException; + public void setNativeToJsBridgeMode(int bridgeSecret, int value) throws IllegalAccessException; + public String retrieveJsMessages(int bridgeSecret, boolean fromOnlineEvent) throws IllegalAccessException; } diff --git a/framework/src/org/apache/cordova/IceCreamCordovaWebViewClient.java b/framework/src/org/apache/cordova/IceCreamCordovaWebViewClient.java index 2501c981..3c6ad215 100644 --- a/framework/src/org/apache/cordova/IceCreamCordovaWebViewClient.java +++ b/framework/src/org/apache/cordova/IceCreamCordovaWebViewClient.java @@ -35,6 +35,7 @@ import android.webkit.WebView; public class IceCreamCordovaWebViewClient extends AndroidWebViewClient implements CordovaWebViewClient{ private static final String TAG = "IceCreamCordovaWebViewClient"; + private CordovaUriHelper helper; public IceCreamCordovaWebViewClient(CordovaInterface cordova) { super(cordova); @@ -47,8 +48,9 @@ public class IceCreamCordovaWebViewClient extends AndroidWebViewClient implement @Override public WebResourceResponse shouldInterceptRequest(WebView view, String url) { try { - // Check the against the white-list. - if ((url.startsWith("http:") || url.startsWith("https:")) && !Config.isUrlWhiteListed(url)) { + // Check the against the whitelist and lock out access to the WebView directory + // Changing this will cause problems for your application + if (isUrlHarmful(url)) { LOG.w(TAG, "URL blocked by whitelist: " + url); // Results in a 404. return new WebResourceResponse("text/plain", "UTF-8", null); @@ -74,6 +76,11 @@ public class IceCreamCordovaWebViewClient extends AndroidWebViewClient implement } } + private boolean isUrlHarmful(String url) { + return ((url.startsWith("http:") || url.startsWith("https:")) && !Config.isUrlWhiteListed(url)) + || url.contains("app_webview"); + } + private static boolean needsKitKatContentUrlFix(Uri uri) { return android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT && "content".equals(uri.getScheme()); } diff --git a/framework/src/org/apache/cordova/NativeToJsMessageQueue.java b/framework/src/org/apache/cordova/NativeToJsMessageQueue.java index 7fb6c065..b822800e 100755 --- a/framework/src/org/apache/cordova/NativeToJsMessageQueue.java +++ b/framework/src/org/apache/cordova/NativeToJsMessageQueue.java @@ -35,31 +35,19 @@ import android.webkit.WebView; public class NativeToJsMessageQueue { private static final String LOG_TAG = "JsMessageQueue"; - // This must match the default value in cordova-js/lib/android/exec.js - private static final int DEFAULT_BRIDGE_MODE = 2; - // Set this to true to force plugin results to be encoding as // JS instead of the custom format (useful for benchmarking). private static final boolean FORCE_ENCODE_USING_EVAL = false; - // Disable URL-based exec() bridge by default since it's a bit of a - // security concern. - public static final boolean ENABLE_LOCATION_CHANGE_EXEC_MODE = false; - // Disable sending back native->JS messages during an exec() when the active // exec() is asynchronous. Set this to true when running bridge benchmarks. - public static final boolean DISABLE_EXEC_CHAINING = false; - + static final boolean DISABLE_EXEC_CHAINING = false; + // Arbitrarily chosen upper limit for how much data to send to JS in one shot. // This currently only chops up on message boundaries. It may be useful // to allow it to break up messages. private static int MAX_PAYLOAD_SIZE = 50 * 1024 * 10240; - /** - * The index into registeredListeners to treat as active. - */ - private int activeListenerIndex; - /** * When true, the active listener is not fired upon enqueue. When set to false, * the active listener will be fired if the queue is non-empty. @@ -76,6 +64,13 @@ public class NativeToJsMessageQueue { */ private final BridgeMode[] registeredListeners; + /** + * When null, the bridge is disabled. This occurs during page transitions. + * When disabled, all callbacks are dropped since they are assumed to be + * relevant to the previous page. + */ + private BridgeMode activeBridgeMode; + private final CordovaInterface cordova; private final CordovaWebView webView; @@ -94,17 +89,19 @@ public class NativeToJsMessageQueue { * Changes the bridge mode. */ public void setBridgeMode(int value) { - if (value < 0 || value >= registeredListeners.length) { + if (value < -1 || value >= registeredListeners.length) { Log.d(LOG_TAG, "Invalid NativeToJsBridgeMode: " + value); } else { - if (value != activeListenerIndex) { - Log.d(LOG_TAG, "Set native->JS mode to " + value); + BridgeMode newMode = value < 0 ? null : registeredListeners[value]; + if (newMode != activeBridgeMode) { + Log.d(LOG_TAG, "Set native->JS mode to " + (newMode == null ? "null" : newMode.getClass().getSimpleName())); synchronized (this) { - activeListenerIndex = value; - BridgeMode activeListener = registeredListeners[value]; - activeListener.reset(); - if (!paused && !queue.isEmpty()) { - activeListener.onNativeToJsMessageAvailable(); + activeBridgeMode = newMode; + if (newMode != null) { + newMode.reset(); + if (!paused && !queue.isEmpty()) { + newMode.onNativeToJsMessageAvailable(); + } } } } @@ -117,8 +114,7 @@ public class NativeToJsMessageQueue { public void reset() { synchronized (this) { queue.clear(); - setBridgeMode(DEFAULT_BRIDGE_MODE); - registeredListeners[activeListenerIndex].reset(); + setBridgeMode(-1); } } @@ -142,7 +138,10 @@ public class NativeToJsMessageQueue { */ public String popAndEncode(boolean fromOnlineEvent) { synchronized (this) { - registeredListeners[activeListenerIndex].notifyOfFlush(fromOnlineEvent); + if (activeBridgeMode == null) { + return null; + } + activeBridgeMode.notifyOfFlush(fromOnlineEvent); if (queue.isEmpty()) { return null; } @@ -247,16 +246,20 @@ public class NativeToJsMessageQueue { enqueueMessage(message); } - + private void enqueueMessage(JsMessage message) { synchronized (this) { + if (activeBridgeMode == null) { + Log.d(LOG_TAG, "Dropping Native->JS message due to disabled bridge"); + return; + } queue.add(message); if (!paused) { - registeredListeners[activeListenerIndex].onNativeToJsMessageAvailable(); + activeBridgeMode.onNativeToJsMessageAvailable(); } - } + } } - + public void setPaused(boolean value) { if (paused && value) { // This should never happen. If a use-case for it comes up, we should @@ -266,16 +269,12 @@ public class NativeToJsMessageQueue { paused = value; if (!value) { synchronized (this) { - if (!queue.isEmpty()) { - registeredListeners[activeListenerIndex].onNativeToJsMessageAvailable(); + if (!queue.isEmpty() && activeBridgeMode != null) { + activeBridgeMode.onNativeToJsMessageAvailable(); } } } } - - public boolean getPaused() { - return paused; - } private abstract class BridgeMode { abstract void onNativeToJsMessageAvailable(); @@ -308,23 +307,28 @@ public class NativeToJsMessageQueue { /** Uses online/offline events to tell the JS when to poll for messages. */ private class OnlineEventsBridgeMode extends BridgeMode { private boolean online; - final Runnable runnable = new Runnable() { + private boolean ignoreNextFlush; + + final Runnable toggleNetworkRunnable = new Runnable() { public void run() { if (!queue.isEmpty()) { + ignoreNextFlush = false; webView.setNetworkAvailable(online); } - } + } }; @Override void reset() { online = false; + // If the following call triggers a notifyOfFlush, then ignore it. + ignoreNextFlush = true; webView.setNetworkAvailable(true); } @Override void onNativeToJsMessageAvailable() { - cordova.getActivity().runOnUiThread(runnable); + cordova.getActivity().runOnUiThread(toggleNetworkRunnable); } // Track when online/offline events are fired so that we don't fire excess events. @Override void notifyOfFlush(boolean fromOnlineEvent) { - if (fromOnlineEvent) { + if (fromOnlineEvent && !ignoreNextFlush) { online = !online; } } diff --git a/framework/src/org/apache/cordova/PluginManager.java b/framework/src/org/apache/cordova/PluginManager.java index 50fe6b79..c8cecf22 100755 --- a/framework/src/org/apache/cordova/PluginManager.java +++ b/framework/src/org/apache/cordova/PluginManager.java @@ -23,10 +23,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; -import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; -import org.apache.cordova.CordovaArgs; import org.apache.cordova.CordovaWebView; import org.apache.cordova.CallbackContext; import org.apache.cordova.CordovaInterface; @@ -65,8 +62,6 @@ public class PluginManager { // Using is deprecated. protected HashMap> urlMap = new HashMap>(); - private AtomicInteger numPendingUiExecs; - private Set pluginIdWhitelist; /** @@ -79,7 +74,6 @@ public class PluginManager { this.ctx = ctx; this.app = app; this.firstRun = true; - this.numPendingUiExecs = new AtomicInteger(0); } public void setPluginIdWhitelist(Set pluginIdWhitelist) { @@ -105,9 +99,6 @@ public class PluginManager { this.clearPluginObjects(); } - // Insert PluginManager service - this.addService(new PluginEntry("PluginManager", new PluginManagerService())); - // Start up all plugins that have onload specified this.startupPlugins(); } @@ -224,20 +215,6 @@ public class PluginManager { * plugin execute method. */ public void exec(final String service, final String action, final String callbackId, final String rawArgs) { - if (numPendingUiExecs.get() > 0) { - numPendingUiExecs.getAndIncrement(); - this.ctx.getActivity().runOnUiThread(new Runnable() { - public void run() { - execHelper(service, action, callbackId, rawArgs); - numPendingUiExecs.getAndDecrement(); - } - }); - } else { - execHelper(service, action, callbackId, rawArgs); - } - } - - private void execHelper(final String service, final String action, final String callbackId, final String rawArgs) { CordovaPlugin plugin = getPlugin(service); if (plugin == null) { Log.d(TAG, "exec() call to unknown plugin: " + service); @@ -449,26 +426,4 @@ public class PluginManager { } return null; } - - private class PluginManagerService extends CordovaPlugin { - @Override - public boolean execute(String action, CordovaArgs args, final CallbackContext callbackContext) throws JSONException { - if ("startup".equals(action)) { - // The onPageStarted event of CordovaWebViewClient resets the queue of messages to be returned to javascript in response - // to exec calls. Since this event occurs on the UI thread and exec calls happen on the WebCore thread it is possible - // that onPageStarted occurs after exec calls have started happening on a new page, which can cause the message queue - // to be reset between the queuing of a new message and its retrieval by javascript. To avoid this from happening, - // javascript always sends a "startup" exec upon loading a new page which causes all future exec calls to happen on the UI - // thread (and hence after onPageStarted) until there are no more pending exec calls remaining. - numPendingUiExecs.getAndIncrement(); - ctx.getActivity().runOnUiThread(new Runnable() { - public void run() { - numPendingUiExecs.getAndDecrement(); - } - }); - return true; - } - return false; - } - } }