Commit 442aea6e authored by Clark DuVall's avatar Clark DuVall Committed by Commit Bot

[WebLayer] Implement executeScript() on BrowserController

This adds a way to execute javascript in WebLayer. The script will be
executed in a weblayer isolate that will not be able to access any
scripts from the page.

Change-Id: Ia17bd8476b5d9247d5761f0af24cd4d61edaccb2
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1867300
Commit-Queue: Clark DuVall <cduvall@chromium.org>
Reviewed-by: default avatarScott Violet <sky@chromium.org>
Cr-Commit-Position: refs/heads/master@{#707455}
parent b58ed368
......@@ -44,6 +44,7 @@ jumbo_static_library("weblayer_lib") {
"browser/browser_main_parts_impl.h",
"browser/content_browser_client_impl.cc",
"browser/content_browser_client_impl.h",
"browser/isolated_world_ids.h",
"browser/navigation_controller_impl.cc",
"browser/navigation_controller_impl.h",
"browser/navigation_impl.cc",
......
......@@ -21,7 +21,10 @@
#endif
#if defined(OS_ANDROID)
#include "base/android/callback_android.h"
#include "base/android/jni_string.h"
#include "base/json/json_writer.h"
#include "weblayer/browser/isolated_world_ids.h"
#include "weblayer/browser/java/jni/BrowserControllerImpl_jni.h"
#include "weblayer/browser/top_controls_container_view.h"
#endif
......@@ -40,6 +43,14 @@ struct UserData : public base::SupportsUserData::Data {
#if defined(OS_ANDROID)
BrowserController* g_last_browser_controller;
void JavaScriptResultCallback(
const base::android::ScopedJavaGlobalRef<jobject>& callback,
base::Value result) {
std::string json;
base::JSONWriter::Write(result, &json);
base::android::RunStringCallbackAndroid(callback, json);
}
#endif
} // namespace
......@@ -149,6 +160,17 @@ void BrowserControllerImpl::SetTopControlsContainerView(
native_top_controls_container_view);
}
void BrowserControllerImpl::ExecuteScript(
JNIEnv* env,
const base::android::JavaParamRef<jstring>& script,
const base::android::JavaParamRef<jobject>& callback) {
base::android::ScopedJavaGlobalRef<jobject> jcallback(env, callback);
web_contents_->GetMainFrame()->ExecuteJavaScriptInIsolatedWorld(
base::android::ConvertJavaStringToUTF16(script),
base::BindOnce(&JavaScriptResultCallback, jcallback),
ISOLATED_WORLD_ID_WEBLAYER);
}
#endif
void BrowserControllerImpl::LoadingStateChanged(content::WebContents* source,
......
......@@ -54,6 +54,9 @@ class BrowserControllerImpl : public BrowserController,
JNIEnv* env,
const base::android::JavaParamRef<jobject>& caller,
jlong native_top_controls_container_view);
void ExecuteScript(JNIEnv* env,
const base::android::JavaParamRef<jstring>& script,
const base::android::JavaParamRef<jobject>& callback);
#endif
DownloadDelegate* download_delegate() { return download_delegate_; }
......
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef WEBLAYER_BROWSER_ISOLATED_WORLD_IDS_H_
#define WEBLAYER_BROWSER_ISOLATED_WORLD_IDS_H_
#include "content/public/common/isolated_world_ids.h"
namespace weblayer {
enum IsolatedWorldIDs {
ISOLATED_WORLD_ID_WEBLAYER = content::ISOLATED_WORLD_ID_CONTENT_END + 1,
};
} // namespace weblayer
#endif // WEBLAYER_BROWSER_ISOLATED_WORLD_IDS_H_
......@@ -13,6 +13,7 @@ import android.view.ViewGroup.LayoutParams;
import android.webkit.ValueCallback;
import android.widget.FrameLayout;
import org.chromium.base.Callback;
import org.chromium.base.annotations.JNINamespace;
import org.chromium.base.annotations.NativeMethods;
import org.chromium.content_public.browser.ViewEventSink;
......@@ -161,6 +162,22 @@ public final class BrowserControllerImpl extends IBrowserController.Stub {
}
}
@Override
public void executeScript(String script, IObjectWrapper callback) {
Callback<String> nativeCallback = new Callback<String>() {
@Override
public void onResult(String result) {
ValueCallback<String> unwrappedCallback =
(ValueCallback<String>) ObjectWrapper.unwrap(callback, ValueCallback.class);
if (unwrappedCallback != null) {
unwrappedCallback.onReceiveValue(result);
}
}
};
BrowserControllerImplJni.get().executeScript(
mNativeBrowserController, script, nativeCallback);
}
public void destroy() {
BrowserControllerImplJni.get().setTopControlsContainerView(
mNativeBrowserController, BrowserControllerImpl.this, 0);
......@@ -206,5 +223,7 @@ public final class BrowserControllerImpl extends IBrowserController.Stub {
BrowserControllerImpl caller, long nativeTopControlsContainerView);
void deleteBrowserController(long browserController);
WebContents getWebContents(long nativeBrowserControllerImpl, BrowserControllerImpl caller);
void executeScript(
long nativeBrowserControllerImpl, String script, Callback<String> callback);
}
}
......@@ -17,4 +17,6 @@ interface IBrowserController {
void setDownloadDelegateClient(IDownloadDelegateClient client) = 2;
void setFullscreenDelegateClient(in IFullscreenDelegateClient client) = 3;
void executeScript(in String script, in IObjectWrapper callback) = 4;
}
......@@ -8,6 +8,9 @@ import android.net.Uri;
import android.os.RemoteException;
import android.webkit.ValueCallback;
import org.json.JSONException;
import org.json.JSONObject;
import org.chromium.weblayer_private.aidl.APICallException;
import org.chromium.weblayer_private.aidl.IBrowserController;
import org.chromium.weblayer_private.aidl.IBrowserControllerClient;
......@@ -17,6 +20,9 @@ import org.chromium.weblayer_private.aidl.IObjectWrapper;
import org.chromium.weblayer_private.aidl.ObjectWrapper;
public final class BrowserController {
/** The top level key of the JSON object returned by executeScript(). */
public static final String SCRIPT_RESULT_KEY = "result";
private final IBrowserController mImpl;
private FullscreenDelegateClientImpl mFullscreenDelegateClient;
private final NavigationController mNavigationController;
......@@ -66,6 +72,32 @@ public final class BrowserController {
return mDownloadDelegateClient != null ? mDownloadDelegateClient.getDelegate() : null;
}
/**
* Executes the script in an isolated world, and returns the result as a JSON object to the
* callback if provided. The object passed to the callback will have a single key
* SCRIPT_RESULT_KEY which will hold the result of running the script.
*/
public void executeScript(String script, ValueCallback<JSONObject> callback) {
try {
ValueCallback<String> stringCallback = (String result) -> {
if (callback == null) {
return;
}
try {
callback.onReceiveValue(
new JSONObject("{\"" + SCRIPT_RESULT_KEY + "\":" + result + "}"));
} catch (JSONException e) {
// This should never happen since the result should be well formed.
throw new RuntimeException(e);
}
};
mImpl.executeScript(script, ObjectWrapper.wrap(stringCallback));
} catch (RemoteException e) {
throw new APICallException(e);
}
}
public FullscreenDelegate getFullscreenDelegate() {
return mFullscreenDelegateClient != null ? mFullscreenDelegateClient.getDelegate() : null;
}
......
......@@ -258,6 +258,7 @@ instrumentation_test_apk("weblayer_instrumentation_test_apk") {
"javatests/src/org/chromium/weblayer/test/BrowserObserverTest.java",
"javatests/src/org/chromium/weblayer/test/NavigationTest.java",
"javatests/src/org/chromium/weblayer/test/SmokeTest.java",
"javatests/src/org/chromium/weblayer/test/ExecuteScriptTest.java",
"javatests/src/org/chromium/weblayer/test/RenderingTest.java",
"javatests/src/org/chromium/weblayer/test/WebLayerShellActivityTestRule.java",
"javatests/src/org/chromium/weblayer/test/FragmentRestoreTest.java",
......
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.weblayer.test;
import android.support.test.filters.SmallTest;
import org.json.JSONObject;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.base.test.BaseJUnit4ClassRunner;
import org.chromium.base.test.util.UrlUtils;
import org.chromium.content_public.browser.test.util.TestThreadUtils;
import org.chromium.weblayer.BrowserController;
import org.chromium.weblayer.shell.WebLayerShellActivity;
/**
* Tests that script execution works as expected.
*/
@RunWith(BaseJUnit4ClassRunner.class)
public class ExecuteScriptTest {
@Rule
public WebLayerShellActivityTestRule mActivityTestRule = new WebLayerShellActivityTestRule();
private static final String DATA_URL = UrlUtils.encodeHtmlDataUri(
"<html><head><script>var bar = 10;</script></head><body>foo</body></html>");
@Test
@SmallTest
public void testBasicScript() throws Exception {
WebLayerShellActivity activity = mActivityTestRule.launchShellWithUrl(DATA_URL);
mActivityTestRule.waitForNavigation(DATA_URL);
JSONObject result = mActivityTestRule.executeScriptSync("document.body.innerHTML");
Assert.assertEquals(result.getString(BrowserController.SCRIPT_RESULT_KEY), "foo");
}
@Test
@SmallTest
public void testScriptIsolatedFromPage() throws Exception {
WebLayerShellActivity activity = mActivityTestRule.launchShellWithUrl(DATA_URL);
mActivityTestRule.waitForNavigation(DATA_URL);
JSONObject result = mActivityTestRule.executeScriptSync("bar");
Assert.assertTrue(result.isNull(BrowserController.SCRIPT_RESULT_KEY));
}
@Test
@SmallTest
public void testScriptNotIsolatedFromOtherScript() throws Exception {
WebLayerShellActivity activity = mActivityTestRule.launchShellWithUrl(DATA_URL);
mActivityTestRule.waitForNavigation(DATA_URL);
mActivityTestRule.executeScriptSync("var foo = 20;");
JSONObject result = mActivityTestRule.executeScriptSync("foo");
Assert.assertEquals(result.getInt(BrowserController.SCRIPT_RESULT_KEY), 20);
}
@Test
@SmallTest
public void testClearedOnNavigate() throws Exception {
WebLayerShellActivity activity = mActivityTestRule.launchShellWithUrl(DATA_URL);
mActivityTestRule.waitForNavigation(DATA_URL);
mActivityTestRule.executeScriptSync("var foo = 20;");
String newUrl = UrlUtils.encodeHtmlDataUri("<html></html>");
mActivityTestRule.loadUrl(newUrl);
mActivityTestRule.waitForNavigation(newUrl);
JSONObject result = mActivityTestRule.executeScriptSync("foo");
Assert.assertTrue(result.isNull(BrowserController.SCRIPT_RESULT_KEY));
}
@Test
@SmallTest
public void testNullCallback() throws Exception {
WebLayerShellActivity activity = mActivityTestRule.launchShellWithUrl(DATA_URL);
mActivityTestRule.waitForNavigation(DATA_URL);
TestThreadUtils.runOnUiThreadBlocking(() -> {
// Null callback should not crash.
activity.getBrowserController().executeScript("null", null);
});
// Execute a sync script to make sure the other script finishes.
mActivityTestRule.executeScriptSync("null");
}
}
......@@ -14,6 +14,9 @@ import android.net.Uri;
import android.support.test.InstrumentationRegistry;
import android.support.test.rule.ActivityTestRule;
import org.json.JSONObject;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.content_public.browser.test.util.Criteria;
import org.chromium.content_public.browser.test.util.CriteriaHelper;
import org.chromium.content_public.browser.test.util.TestThreadUtils;
......@@ -21,6 +24,7 @@ import org.chromium.weblayer.NavigationController;
import org.chromium.weblayer.shell.WebLayerShellActivity;
import java.lang.reflect.Field;
import java.util.concurrent.TimeoutException;
/**
* ActivityTestRule for WebLayerShellActivity.
......@@ -30,6 +34,19 @@ import java.lang.reflect.Field;
public class WebLayerShellActivityTestRule extends ActivityTestRule<WebLayerShellActivity> {
private static final long WAIT_FOR_NAVIGATION_TIMEOUT = 10000L;
private static final class JSONCallbackHelper extends CallbackHelper {
private JSONObject mResult;
public JSONObject getResult() {
return mResult;
}
public void notifyCalled(JSONObject result) {
mResult = result;
notifyCalled();
}
}
public WebLayerShellActivityTestRule() {
super(WebLayerShellActivity.class, false, false);
}
......@@ -102,4 +119,22 @@ public class WebLayerShellActivityTestRule extends ActivityTestRule<WebLayerShel
throw new RuntimeException(e);
}
}
/**
* Executes the script passed in and waits for the result.
*/
public JSONObject executeScriptSync(String script) {
JSONCallbackHelper callbackHelper = new JSONCallbackHelper();
int count = callbackHelper.getCallCount();
TestThreadUtils.runOnUiThreadBlocking(() -> {
getActivity().getBrowserController().executeScript(
script, (JSONObject result) -> { callbackHelper.notifyCalled(result); });
});
try {
callbackHelper.waitForCallback(count);
} catch (TimeoutException e) {
throw new RuntimeException(e);
}
return callbackHelper.getResult();
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment