From b0b628ffc24bdd952e86908cf6cb4064b6f3c405 Mon Sep 17 00:00:00 2001 From: Joe Bowser Date: Tue, 24 Jun 2014 12:30:12 -0700 Subject: [PATCH 1/6] Refactoring the URI handling on Cordova, removing dead code --- .../org/apache/cordova/CordovaUriHelper.java | 112 ++++++++++++++++++ .../org/apache/cordova/CordovaWebView.java | 27 ++--- .../apache/cordova/CordovaWebViewClient.java | 110 +---------------- .../cordova/IceCreamCordovaWebViewClient.java | 11 +- 4 files changed, 133 insertions(+), 127 deletions(-) create mode 100644 framework/src/org/apache/cordova/CordovaUriHelper.java diff --git a/framework/src/org/apache/cordova/CordovaUriHelper.java b/framework/src/org/apache/cordova/CordovaUriHelper.java new file mode 100644 index 00000000..1a113ded --- /dev/null +++ b/framework/src/org/apache/cordova/CordovaUriHelper.java @@ -0,0 +1,112 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +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; + + CordovaUriHelper(CordovaInterface cdv, CordovaWebView webView) + { + appView = webView; + 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); + appView.pluginManager.exec(service, action, callbackId, jsonArgs); + //There is no reason to not send this directly to the pluginManager + } + + + /** + * Give the host application a chance to take over the control when a new url + * is about to be loaded in the current WebView. + * + * @param view The WebView that is initiating the callback. + * @param url The url to be loaded. + * @return true to override, false for default behavior + */ + public boolean shouldOverrideUrlLoading(WebView view, String url) { + // The WebView should support http and https when going on the Internet + 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)) + { + return false; + } + } + // Give plugins the chance to handle the url + else if (this.appView.pluginManager.onOverrideUrlLoading(url)) { + + } + 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 + { + 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); + } + } + //Default behaviour should be to load the default intent, let's see what happens! + return true; + } + + + +} diff --git a/framework/src/org/apache/cordova/CordovaWebView.java b/framework/src/org/apache/cordova/CordovaWebView.java index b31eec57..ad5c0f0e 100755 --- a/framework/src/org/apache/cordova/CordovaWebView.java +++ b/framework/src/org/apache/cordova/CordovaWebView.java @@ -401,17 +401,7 @@ public class CordovaWebView extends WebView { 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); } } @@ -422,16 +412,15 @@ public class CordovaWebView extends WebView { * @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); } } diff --git a/framework/src/org/apache/cordova/CordovaWebViewClient.java b/framework/src/org/apache/cordova/CordovaWebViewClient.java index 407c1fbc..4a72feaf 100755 --- a/framework/src/org/apache/cordova/CordovaWebViewClient.java +++ b/framework/src/org/apache/cordova/CordovaWebViewClient.java @@ -61,6 +61,7 @@ public class CordovaWebViewClient extends WebViewClient { private static final String CORDOVA_EXEC_URL_PREFIX = "http://cdv_exec/"; CordovaInterface cordova; CordovaWebView appView; + CordovaUriHelper helper; private boolean doClearHistory = false; boolean isCurrentlyLoading; @@ -85,6 +86,7 @@ public class CordovaWebViewClient extends WebViewClient { public CordovaWebViewClient(CordovaInterface cordova, CordovaWebView view) { this.cordova = cordova; this.appView = view; + helper = new CordovaUriHelper(cordova, view); } /** @@ -94,6 +96,7 @@ public class CordovaWebViewClient extends WebViewClient { */ public void setWebView(CordovaWebView view) { this.appView = view; + helper = new CordovaUriHelper(cordova, view); } @@ -125,112 +128,7 @@ public class CordovaWebViewClient extends WebViewClient { */ @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.pluginManager != null) && this.appView.pluginManager.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); } /** diff --git a/framework/src/org/apache/cordova/IceCreamCordovaWebViewClient.java b/framework/src/org/apache/cordova/IceCreamCordovaWebViewClient.java index 63cfb911..67793d73 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 CordovaWebViewClient { private static final String TAG = "IceCreamCordovaWebViewClient"; + private CordovaUriHelper helper; public IceCreamCordovaWebViewClient(CordovaInterface cordova) { super(cordova); @@ -47,8 +48,9 @@ public class IceCreamCordovaWebViewClient extends CordovaWebViewClient { @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 CordovaWebViewClient { } } + 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()); } From c47bcb2f54a098f4f428350bc0053fc29ec2efea Mon Sep 17 00:00:00 2001 From: Joe Bowser Date: Tue, 24 Jun 2014 12:55:56 -0700 Subject: [PATCH 2/6] This breaks running the JUnit tests, we'll bring it back soon --- .../cordova/test/junit/MessageTest.java | 73 ------------------- 1 file changed, 73 deletions(-) delete mode 100644 test/src/org/apache/cordova/test/junit/MessageTest.java diff --git a/test/src/org/apache/cordova/test/junit/MessageTest.java b/test/src/org/apache/cordova/test/junit/MessageTest.java deleted file mode 100644 index 1d236963..00000000 --- a/test/src/org/apache/cordova/test/junit/MessageTest.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you under the Apache License, Version 2.0 (the - "License"); you may not use this file except in compliance - with the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, - software distributed under the License is distributed on an - "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - KIND, either express or implied. See the License for the - specific language governing permissions and limitations - under the License. -*/ -package org.apache.cordova.test.junit; - -import org.apache.cordova.CordovaPlugin; -import org.apache.cordova.CordovaWebView; -import org.apache.cordova.ScrollEvent; -import org.apache.cordova.pluginApi.pluginStub; -import org.apache.cordova.test.CordovaWebViewTestActivity; -import org.apache.cordova.test.R; - -import com.robotium.solo.By; -import com.robotium.solo.Solo; - -import android.test.ActivityInstrumentationTestCase2; -import android.view.View; - -public class MessageTest extends -ActivityInstrumentationTestCase2 { - private CordovaWebViewTestActivity testActivity; - private CordovaWebView testView; - private pluginStub testPlugin; - private int TIMEOUT = 1000; - - private Solo solo; - - public MessageTest() { - super("org.apache.cordova.test.activities", CordovaWebViewTestActivity.class); - } - - protected void setUp() throws Exception { - super.setUp(); - testActivity = this.getActivity(); - testView = (CordovaWebView) testActivity.findViewById(R.id.cordovaWebView); - testPlugin = (pluginStub) testView.pluginManager.getPlugin("PluginStub"); - solo = new Solo(getInstrumentation(), getActivity()); - } - - public void testOnScrollChanged() - { - solo.waitForWebElement(By.textContent("Cordova Android Tests")); - solo.scrollDown(); - sleep(); - Object data = testPlugin.data; - assertTrue(data.getClass().getSimpleName().equals("ScrollEvent")); - } - - - - private void sleep() { - try { - Thread.sleep(TIMEOUT); - } catch (InterruptedException e) { - fail("Unexpected Timeout"); - } - } -} From 6f21a96238a298a94cb66bfa4f2a969f768cea69 Mon Sep 17 00:00:00 2001 From: Joe Bowser Date: Tue, 24 Jun 2014 12:57:46 -0700 Subject: [PATCH 3/6] Update the errorurl to no longer use intents --- framework/src/org/apache/cordova/Config.java | 15 ++++++++++----- .../src/org/apache/cordova/CordovaActivity.java | 2 +- 2 files changed, 11 insertions(+), 6 deletions(-) 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 a2610c5a..1cfe265c 100755 --- a/framework/src/org/apache/cordova/CordovaActivity.java +++ b/framework/src/org/apache/cordova/CordovaActivity.java @@ -716,7 +716,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) { From 445ddd89fb3269a772978a9860247065e5886249 Mon Sep 17 00:00:00 2001 From: Andrew Grieve Date: Thu, 3 Jul 2014 13:27:30 -0400 Subject: [PATCH 4/6] CB-6761 Fix native->JS bridge ceasing to fire when page changes and online is set to false and the JS loads quickly --- .../org/apache/cordova/NativeToJsMessageQueue.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/framework/src/org/apache/cordova/NativeToJsMessageQueue.java b/framework/src/org/apache/cordova/NativeToJsMessageQueue.java index 5d0c061b..9f6f96ef 100755 --- a/framework/src/org/apache/cordova/NativeToJsMessageQueue.java +++ b/framework/src/org/apache/cordova/NativeToJsMessageQueue.java @@ -308,23 +308,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; } } From aab47bd4532bfe8707d745638eb5695ac543c681 Mon Sep 17 00:00:00 2001 From: Andrew Grieve Date: Thu, 3 Jul 2014 21:58:35 -0400 Subject: [PATCH 5/6] CB-5988 Allow exec() only from file: or start-up URL's domain Uses prompt() to validate the origin of the calling JS. This change also simplifies the start-up logic by explicitly disabling the bridge during page transitions and explictly enabling it when the JS asks for the bridgeSecret. We now wait to fire onNativeReady in JS until the bridge is initialized. It is therefore safe to delete the queue-clear/new exec race condition code that was in PluginManager. --- .../apache/cordova/CordovaChromeClient.java | 134 ++++++++++-------- .../apache/cordova/CordovaWebViewClient.java | 5 +- .../src/org/apache/cordova/ExposedJsApi.java | 28 +++- .../cordova/NativeToJsMessageQueue.java | 63 ++++---- .../src/org/apache/cordova/PluginManager.java | 44 ------ 5 files changed, 132 insertions(+), 142 deletions(-) diff --git a/framework/src/org/apache/cordova/CordovaChromeClient.java b/framework/src/org/apache/cordova/CordovaChromeClient.java index 2edabf15..f2c33501 100755 --- a/framework/src/org/apache/cordova/CordovaChromeClient.java +++ b/framework/src/org/apache/cordova/CordovaChromeClient.java @@ -28,6 +28,7 @@ import android.app.AlertDialog; import android.content.DialogInterface; import android.content.Intent; import android.net.Uri; +import android.util.Log; import android.view.Gravity; import android.view.KeyEvent; import android.view.View; @@ -201,64 +202,75 @@ public class CordovaChromeClient extends WebChromeClient { * 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); + 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)); - 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)); - 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); @@ -338,10 +350,10 @@ public class CordovaChromeClient extends WebChromeClient { this.appView.showCustomView(view, callback); } - @Override - public void onHideCustomView() { - this.appView.hideCustomView(); - } + @Override + public void onHideCustomView() { + this.appView.hideCustomView(); + } @Override /** @@ -351,24 +363,24 @@ public class CordovaChromeClient extends WebChromeClient { */ 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; } diff --git a/framework/src/org/apache/cordova/CordovaWebViewClient.java b/framework/src/org/apache/cordova/CordovaWebViewClient.java index 4a72feaf..9e276d75 100755 --- a/framework/src/org/apache/cordova/CordovaWebViewClient.java +++ b/framework/src/org/apache/cordova/CordovaWebViewClient.java @@ -18,7 +18,6 @@ */ package org.apache.cordova; -import java.io.ByteArrayInputStream; import java.util.Hashtable; import org.apache.cordova.CordovaInterface; @@ -28,18 +27,15 @@ import org.json.JSONException; import org.json.JSONObject; import android.annotation.TargetApi; -import android.content.Intent; 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; @@ -170,6 +166,7 @@ public class CordovaWebViewClient extends WebViewClient { LOG.d(TAG, "onPageStarted(" + url + ")"); // Flush stale messages. this.appView.jsMessageQueue.reset(); + this.appView.exposedJsApi.clearBridgeSecret(); // Broadcast message that page has loaded this.appView.postMessage("onPageStarted", url); diff --git a/framework/src/org/apache/cordova/ExposedJsApi.java b/framework/src/org/apache/cordova/ExposedJsApi.java index fde57221..97f6038c 100755 --- a/framework/src/org/apache/cordova/ExposedJsApi.java +++ b/framework/src/org/apache/cordova/ExposedJsApi.java @@ -19,6 +19,7 @@ package org.apache.cordova; import android.webkit.JavascriptInterface; + import org.apache.cordova.PluginManager; import org.json.JSONException; @@ -31,6 +32,7 @@ import org.json.JSONException; private PluginManager pluginManager; private NativeToJsMessageQueue jsMessageQueue; + private volatile int bridgeSecret = -1; // written by UI thread, read by JS thread. public ExposedJsApi(PluginManager pluginManager, NativeToJsMessageQueue jsMessageQueue) { this.pluginManager = pluginManager; @@ -38,7 +40,8 @@ import org.json.JSONException; } @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) { @@ -65,12 +68,31 @@ import org.json.JSONException; } @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/NativeToJsMessageQueue.java b/framework/src/org/apache/cordova/NativeToJsMessageQueue.java index 9f6f96ef..063fc7e8 100755 --- a/framework/src/org/apache/cordova/NativeToJsMessageQueue.java +++ b/framework/src/org/apache/cordova/NativeToJsMessageQueue.java @@ -35,9 +35,6 @@ 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; @@ -49,17 +46,12 @@ public class NativeToJsMessageQueue { // Disable sending back native->JS messages during an exec() when the active // exec() is asynchronous. Set this to true when running bridge benchmarks. 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 +68,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 +93,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 +118,7 @@ public class NativeToJsMessageQueue { public void reset() { synchronized (this) { queue.clear(); - setBridgeMode(DEFAULT_BRIDGE_MODE); - registeredListeners[activeListenerIndex].reset(); + setBridgeMode(-1); } } @@ -142,7 +142,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 +250,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 +273,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(); diff --git a/framework/src/org/apache/cordova/PluginManager.java b/framework/src/org/apache/cordova/PluginManager.java index 1ed05237..02536ba3 100755 --- a/framework/src/org/apache/cordova/PluginManager.java +++ b/framework/src/org/apache/cordova/PluginManager.java @@ -23,9 +23,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; -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 +63,6 @@ public class PluginManager { // Using is deprecated. protected HashMap> urlMap = new HashMap>(); - private AtomicInteger numPendingUiExecs; - /** * Constructor. * @@ -77,7 +73,6 @@ public class PluginManager { this.ctx = ctx; this.app = app; this.firstRun = true; - this.numPendingUiExecs = new AtomicInteger(0); } /** @@ -99,9 +94,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(); } @@ -216,20 +208,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); @@ -437,26 +415,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; - } - } } From f577af08864c4503fad5afc7d9ca7b9611ba27ce Mon Sep 17 00:00:00 2001 From: Andrew Grieve Date: Thu, 3 Jul 2014 22:18:18 -0400 Subject: [PATCH 6/6] Delete Location-change JS->Native bridge mode It was always disabled, and there's really no reason to keep it around. --- .../org/apache/cordova/CordovaUriHelper.java | 34 +------------------ .../cordova/NativeToJsMessageQueue.java | 4 --- 2 files changed, 1 insertion(+), 37 deletions(-) diff --git a/framework/src/org/apache/cordova/CordovaUriHelper.java b/framework/src/org/apache/cordova/CordovaUriHelper.java index 1a113ded..1502a1fa 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,27 +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); - appView.pluginManager.exec(service, action, callbackId, jsonArgs); - //There is no reason to not send this directly to the pluginManager - } - - /** * Give the host application a chance to take over the control when a new url * is about to be loaded in the current WebView. @@ -73,12 +48,8 @@ public class CordovaUriHelper { // The WebView should support http and https when going on the Internet 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; } @@ -106,7 +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/NativeToJsMessageQueue.java b/framework/src/org/apache/cordova/NativeToJsMessageQueue.java index 063fc7e8..b822800e 100755 --- a/framework/src/org/apache/cordova/NativeToJsMessageQueue.java +++ b/framework/src/org/apache/cordova/NativeToJsMessageQueue.java @@ -39,10 +39,6 @@ public class NativeToJsMessageQueue { // 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. - 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. static final boolean DISABLE_EXEC_CHAINING = false;