blob: b541b8ef96416e905697433cac5f62c82a227ec5 [file] [log] [blame]
// Copyright 2016 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.
#include "extensions/renderer/native_extension_bindings_system_test_base.h"
#include "base/macros.h"
#include "base/stl_util.h"
#include "base/strings/stringprintf.h"
#include "base/test/bind_test_util.h"
#include "components/crx_file/id_util.h"
#include "extensions/common/extension_api.h"
#include "extensions/common/extension_builder.h"
#include "extensions/common/extension_messages.h"
#include "extensions/common/manifest.h"
#include "extensions/common/permissions/permissions_data.h"
#include "extensions/common/value_builder.h"
#include "extensions/renderer/bindings/api_binding_test_util.h"
#include "extensions/renderer/bindings/api_invocation_errors.h"
#include "extensions/renderer/bindings/api_response_validator.h"
#include "extensions/renderer/bindings/test_js_runner.h"
#include "extensions/renderer/message_target.h"
#include "extensions/renderer/native_extension_bindings_system.h"
#include "extensions/renderer/script_context.h"
namespace extensions {
namespace {
// Returns true if the value specified by |property| exists in the given
// context.
bool PropertyExists(v8::Local<v8::Context> context,
base::StringPiece property) {
v8::Local<v8::Value> value = V8ValueFromScriptSource(context, property);
EXPECT_FALSE(value.IsEmpty());
return !value->IsUndefined();
}
} // namespace
TEST_F(NativeExtensionBindingsSystemUnittest, Basic) {
scoped_refptr<const Extension> extension =
ExtensionBuilder("foo")
.AddPermissions({"idle", "power", "webRequest"})
.Build();
RegisterExtension(extension);
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
ScriptContext* script_context = CreateScriptContext(
context, extension.get(), Feature::BLESSED_EXTENSION_CONTEXT);
script_context->set_url(extension->url());
bindings_system()->UpdateBindingsForContext(script_context);
// chrome.idle.queryState should exist.
v8::Local<v8::Value> chrome =
GetPropertyFromObject(context->Global(), context, "chrome");
ASSERT_FALSE(chrome.IsEmpty());
ASSERT_TRUE(chrome->IsObject());
v8::Local<v8::Value> idle = GetPropertyFromObject(
v8::Local<v8::Object>::Cast(chrome), context, "idle");
ASSERT_FALSE(idle.IsEmpty());
ASSERT_TRUE(idle->IsObject());
v8::Local<v8::Object> idle_object = v8::Local<v8::Object>::Cast(idle);
v8::Local<v8::Value> idle_query_state =
GetPropertyFromObject(idle_object, context, "queryState");
ASSERT_FALSE(idle_query_state.IsEmpty());
EXPECT_EQ(ReplaceSingleQuotes(
"{'ACTIVE':'active','IDLE':'idle','LOCKED':'locked'}"),
GetStringPropertyFromObject(idle_object, context, "IdleState"));
{
// Try calling the function with an invalid invocation - an error should be
// thrown.
const char kCallIdleQueryStateInvalid[] =
"(function() {\n"
" chrome.idle.queryState('foo', function(state) {\n"
" this.responseState = state;\n"
" });\n"
"});";
v8::Local<v8::Function> function =
FunctionFromString(context, kCallIdleQueryStateInvalid);
ASSERT_FALSE(function.IsEmpty());
RunFunctionAndExpectError(
function, context, 0, nullptr,
"Uncaught TypeError: " +
api_errors::InvocationError(
"idle.queryState",
"integer detectionIntervalInSeconds, function callback",
api_errors::NoMatchingSignature()));
}
{
// Call the function correctly.
const char kCallIdleQueryState[] =
"(function() {\n"
" chrome.idle.queryState(30, function(state) {\n"
" this.responseState = state;\n"
" });\n"
"});";
v8::Local<v8::Function> call_idle_query_state =
FunctionFromString(context, kCallIdleQueryState);
RunFunctionOnGlobal(call_idle_query_state, context, 0, nullptr);
}
// Validate the params that would be sent to the browser.
EXPECT_EQ(extension->id(), last_params().extension_id);
EXPECT_EQ("idle.queryState", last_params().name);
EXPECT_EQ(extension->url(), last_params().source_url);
EXPECT_TRUE(last_params().has_callback);
EXPECT_TRUE(
last_params().arguments.Equals(ListValueFromString("[30]").get()));
// Respond and validate.
bindings_system()->HandleResponse(last_params().request_id, true,
*ListValueFromString("['active']"),
std::string());
std::unique_ptr<base::Value> result_value = GetBaseValuePropertyFromObject(
context->Global(), context, "responseState");
ASSERT_TRUE(result_value);
EXPECT_EQ("\"active\"", ValueToString(*result_value));
// Sanity-check that another API also exists as expected.
v8::Local<v8::Value> power_api =
V8ValueFromScriptSource(context, "chrome.power");
ASSERT_FALSE(power_api.IsEmpty());
ASSERT_TRUE(power_api->IsObject());
v8::Local<v8::Value> request_keep_awake = GetPropertyFromObject(
power_api.As<v8::Object>(), context, "requestKeepAwake");
ASSERT_FALSE(request_keep_awake.IsEmpty());
EXPECT_TRUE(request_keep_awake->IsFunction());
// Test properties exposed on the API object itself.
v8::Local<v8::Value> web_request =
V8ValueFromScriptSource(context, "chrome.webRequest");
ASSERT_FALSE(web_request.IsEmpty());
ASSERT_TRUE(web_request->IsObject());
EXPECT_EQ("20", GetStringPropertyFromObject(
web_request.As<v8::Object>(), context,
"MAX_HANDLER_BEHAVIOR_CHANGED_CALLS_PER_10_MINUTES"));
}
TEST_F(NativeExtensionBindingsSystemUnittest, Events) {
scoped_refptr<const Extension> extension =
ExtensionBuilder("foo").AddPermissions({"idle", "power"}).Build();
RegisterExtension(extension);
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
ScriptContext* script_context = CreateScriptContext(
context, extension.get(), Feature::BLESSED_EXTENSION_CONTEXT);
script_context->set_url(extension->url());
bindings_system()->UpdateBindingsForContext(script_context);
{
const char kAddStateChangedListeners[] =
"(function() {\n"
" chrome.idle.onStateChanged.addListener(function() {\n"
" this.didThrow = true;\n"
" throw new Error('Error!!!');\n"
" });\n"
" chrome.idle.onStateChanged.addListener(function(newState) {\n"
" this.newState = newState;\n"
" });\n"
"});";
v8::Local<v8::Function> add_listeners =
FunctionFromString(context, kAddStateChangedListeners);
RunFunctionOnGlobal(add_listeners, context, 0, nullptr);
}
{
TestJSRunner::AllowErrors allow_errors;
bindings_system()->DispatchEventInContext(
"idle.onStateChanged", ListValueFromString("['idle']").get(), nullptr,
script_context);
}
EXPECT_EQ("\"idle\"", GetStringPropertyFromObject(context->Global(), context,
"newState"));
EXPECT_EQ("true", GetStringPropertyFromObject(context->Global(), context,
"didThrow"));
}
// Tests that referencing the same API multiple times returns the same object;
// i.e. chrome.foo === chrome.foo.
TEST_F(NativeExtensionBindingsSystemUnittest, APIObjectsAreEqual) {
scoped_refptr<const Extension> extension =
ExtensionBuilder("foo").AddPermission("idle").Build();
RegisterExtension(extension);
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
ScriptContext* script_context = CreateScriptContext(
context, extension.get(), Feature::BLESSED_EXTENSION_CONTEXT);
script_context->set_url(extension->url());
bindings_system()->UpdateBindingsForContext(script_context);
v8::Local<v8::Value> first_idle_object =
V8ValueFromScriptSource(context, "chrome.idle");
ASSERT_FALSE(first_idle_object.IsEmpty());
EXPECT_TRUE(first_idle_object->IsObject());
EXPECT_FALSE(first_idle_object->IsUndefined());
v8::Local<v8::Value> second_idle_object =
V8ValueFromScriptSource(context, "chrome.idle");
EXPECT_TRUE(first_idle_object == second_idle_object);
}
// Tests that referencing APIs after the context data is disposed is safe (and
// returns undefined if not yet instantiated).
TEST_F(NativeExtensionBindingsSystemUnittest,
ReferencingAPIAfterDisposingContext) {
scoped_refptr<const Extension> extension =
ExtensionBuilder("foo").AddPermissions({"idle", "power"}).Build();
RegisterExtension(extension);
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
ScriptContext* script_context = CreateScriptContext(
context, extension.get(), Feature::BLESSED_EXTENSION_CONTEXT);
script_context->set_url(extension->url());
bindings_system()->UpdateBindingsForContext(script_context);
v8::Local<v8::Value> first_idle_object =
V8ValueFromScriptSource(context, "chrome.idle");
ASSERT_FALSE(first_idle_object.IsEmpty());
EXPECT_TRUE(first_idle_object->IsObject());
DisposeContext(context);
{
// Despite disposal, the context has been kept alive via the Local above.
v8::Context::Scope context_scope(context);
// Check an API that was instantiated....
v8::Local<v8::Value> second_idle_object =
V8ValueFromScriptSource(context, "chrome.idle");
EXPECT_EQ(first_idle_object, second_idle_object);
// ... and also one that wasn't.
v8::Local<v8::Value> power_object =
V8ValueFromScriptSource(context, "chrome.power");
ASSERT_FALSE(power_object.IsEmpty());
EXPECT_TRUE(power_object->IsUndefined());
}
}
// Tests that traditional custom bindings can be used with the native bindings
// system.
TEST_F(NativeExtensionBindingsSystemUnittest, TestBridgingToJSCustomBindings) {
// Custom binding code. This basically utilizes the interface in binding.js in
// order to test backwards compatibility.
const char kCustomBinding[] =
"apiBridge.registerCustomHook((api, extensionId, contextType) => {\n"
" api.apiFunctions.setHandleRequest('queryState',\n"
" (time, callback) => {\n"
" this.timeArg = time;\n"
" callback('active');\n"
" });\n"
" api.apiFunctions.setUpdateArgumentsPreValidate(\n"
" 'setDetectionInterval', (interval) => {\n"
" this.intervalArg = interval;\n"
" return [50];\n"
" });\n"
" this.hookedExtensionId = extensionId;\n"
" this.hookedContextType = contextType;\n"
" api.compiledApi.hookedApiProperty = 'someProperty';\n"
"});\n";
source_map()->RegisterModule("idle", kCustomBinding);
scoped_refptr<const Extension> extension =
ExtensionBuilder("foo").AddPermission("idle").Build();
RegisterExtension(extension);
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
ScriptContext* script_context = CreateScriptContext(
context, extension.get(), Feature::BLESSED_EXTENSION_CONTEXT);
script_context->set_url(extension->url());
bindings_system()->UpdateBindingsForContext(script_context);
{
// Call the function correctly.
const char kCallIdleQueryState[] =
"(function() {\n"
" chrome.idle.queryState(30, function(state) {\n"
" this.responseState = state;\n"
" });\n"
"});";
v8::Local<v8::Function> call_idle_query_state =
FunctionFromString(context, kCallIdleQueryState);
RunFunctionOnGlobal(call_idle_query_state, context, 0, nullptr);
}
// To start, check that the properties we set when running the hooks are
// correct. We do this after calling the function because the API objects (and
// thus the hooks) are set up lazily.
v8::Local<v8::Object> global = context->Global();
EXPECT_EQ(base::StringPrintf("\"%s\"", extension->id().c_str()),
GetStringPropertyFromObject(global, context, "hookedExtensionId"));
EXPECT_EQ("\"BLESSED_EXTENSION\"",
GetStringPropertyFromObject(global, context, "hookedContextType"));
v8::Local<v8::Value> idle_api =
V8ValueFromScriptSource(context, "chrome.idle");
ASSERT_FALSE(idle_api.IsEmpty());
ASSERT_TRUE(idle_api->IsObject());
EXPECT_EQ("\"someProperty\"",
GetStringPropertyFromObject(idle_api.As<v8::Object>(), context,
"hookedApiProperty"));
// Next, we need to check two pieces: first, that the custom handler was
// called with the proper arguments....
EXPECT_EQ("30", GetStringPropertyFromObject(global, context, "timeArg"));
// ...and second, that the callback was called with the proper result.
EXPECT_EQ("\"active\"",
GetStringPropertyFromObject(global, context, "responseState"));
// Test the updateArgumentsPreValidate hook.
{
// Call the function correctly.
const char kCallIdleSetInterval[] =
"(function() {\n"
" chrome.idle.setDetectionInterval(20);\n"
"});";
v8::Local<v8::Function> call_idle_set_interval =
FunctionFromString(context, kCallIdleSetInterval);
RunFunctionOnGlobal(call_idle_set_interval, context, 0, nullptr);
}
// Since we don't have a custom request handler, the hook should have only
// updated the arguments. The request then should have gone to the browser
// normally.
EXPECT_EQ("20", GetStringPropertyFromObject(global, context, "intervalArg"));
EXPECT_EQ(extension->id(), last_params().extension_id);
EXPECT_EQ("idle.setDetectionInterval", last_params().name);
EXPECT_EQ(extension->url(), last_params().source_url);
EXPECT_FALSE(last_params().has_callback);
EXPECT_TRUE(
last_params().arguments.Equals(ListValueFromString("[50]").get()));
}
TEST_F(NativeExtensionBindingsSystemUnittest, TestSendRequestHook) {
// Custom binding code. This basically utilizes the interface in binding.js in
// order to test backwards compatibility.
const char kCustomBinding[] =
"apiBridge.registerCustomHook((api) => {\n"
" api.apiFunctions.setHandleRequest('queryState',\n"
" (time, callback) => {\n"
" bindingUtil.sendRequest('idle.queryState', [time, callback],\n"
" undefined, undefined);\n"
" });\n"
"});\n";
source_map()->RegisterModule("idle", kCustomBinding);
scoped_refptr<const Extension> extension =
ExtensionBuilder("foo").AddPermission("idle").Build();
RegisterExtension(extension);
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
ScriptContext* script_context = CreateScriptContext(
context, extension.get(), Feature::BLESSED_EXTENSION_CONTEXT);
script_context->set_url(extension->url());
bindings_system()->UpdateBindingsForContext(script_context);
{
// Call the function correctly.
const char kCallIdleQueryState[] =
"(function() { chrome.idle.queryState(30, function() {}); });";
v8::Local<v8::Function> call_idle_query_state =
FunctionFromString(context, kCallIdleQueryState);
RunFunctionOnGlobal(call_idle_query_state, context, 0, nullptr);
}
EXPECT_EQ(extension->id(), last_params().extension_id);
EXPECT_EQ("idle.queryState", last_params().name);
EXPECT_EQ(extension->url(), last_params().source_url);
EXPECT_TRUE(last_params().has_callback);
EXPECT_TRUE(
last_params().arguments.Equals(ListValueFromString("[30]").get()));
}
// Tests that we can notify the browser as event listeners are added or removed.
// Note: the notification logic is tested more thoroughly in the APIEventHandler
// unittests.
TEST_F(NativeExtensionBindingsSystemUnittest, TestEventRegistration) {
scoped_refptr<const Extension> extension =
ExtensionBuilder("foo").AddPermissions({"idle", "power"}).Build();
RegisterExtension(extension);
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
ScriptContext* script_context = CreateScriptContext(
context, extension.get(), Feature::BLESSED_EXTENSION_CONTEXT);
script_context->set_url(extension->url());
bindings_system()->UpdateBindingsForContext(script_context);
// Add a new event listener. We should be notified of the change.
const char kEventName[] = "idle.onStateChanged";
v8::Local<v8::Function> listener =
FunctionFromString(context, "(function() {})");
const char kAddListener[] =
"(function(listener) {\n"
" chrome.idle.onStateChanged.addListener(listener);\n"
"});";
v8::Local<v8::Function> add_listener =
FunctionFromString(context, kAddListener);
EXPECT_CALL(*ipc_message_sender(),
SendAddUnfilteredEventListenerIPC(script_context, kEventName))
.Times(1);
v8::Local<v8::Value> argv[] = {listener};
RunFunction(add_listener, context, base::size(argv), argv);
::testing::Mock::VerifyAndClearExpectations(ipc_message_sender());
EXPECT_TRUE(bindings_system()->HasEventListenerInContext(
"idle.onStateChanged", script_context));
// Remove the event listener. We should be notified again.
const char kRemoveListener[] =
"(function(listener) {\n"
" chrome.idle.onStateChanged.removeListener(listener);\n"
"});";
EXPECT_CALL(*ipc_message_sender(),
SendRemoveUnfilteredEventListenerIPC(script_context, kEventName))
.Times(1);
v8::Local<v8::Function> remove_listener =
FunctionFromString(context, kRemoveListener);
RunFunction(remove_listener, context, base::size(argv), argv);
::testing::Mock::VerifyAndClearExpectations(ipc_message_sender());
EXPECT_FALSE(bindings_system()->HasEventListenerInContext(
"idle.onStateChanged", script_context));
}
TEST_F(NativeExtensionBindingsSystemUnittest,
TestPrefixedApiEventsAndAppBinding) {
scoped_refptr<const Extension> app =
ExtensionBuilder("foo", ExtensionBuilder::Type::PLATFORM_APP).Build();
EXPECT_TRUE(app->is_platform_app());
RegisterExtension(app);
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
ScriptContext* script_context = CreateScriptContext(
context, app.get(), Feature::BLESSED_EXTENSION_CONTEXT);
script_context->set_url(app->url());
bindings_system()->UpdateBindingsForContext(script_context);
// The 'chrome.app' object should have 'runtime' and 'window' entries, but
// not the internal 'currentWindowInternal' object.
v8::Local<v8::Value> app_binding_keys =
V8ValueFromScriptSource(context,
"JSON.stringify(Object.keys(chrome.app));");
ASSERT_FALSE(app_binding_keys.IsEmpty());
ASSERT_TRUE(app_binding_keys->IsString());
EXPECT_EQ("[\"runtime\",\"window\"]",
gin::V8ToString(isolate(), app_binding_keys));
const char kUseAppRuntime[] =
"(function() {\n"
" chrome.app.runtime.onLaunched.addListener(function() {});\n"
"});";
v8::Local<v8::Function> use_app_runtime =
FunctionFromString(context, kUseAppRuntime);
EXPECT_CALL(*ipc_message_sender(),
SendAddUnfilteredEventListenerIPC(script_context,
"app.runtime.onLaunched"))
.Times(1);
RunFunctionOnGlobal(use_app_runtime, context, 0, nullptr);
::testing::Mock::VerifyAndClearExpectations(ipc_message_sender());
}
TEST_F(NativeExtensionBindingsSystemUnittest,
TestPrefixedApiMethodsAndSystemBinding) {
scoped_refptr<const Extension> extension =
ExtensionBuilder("foo").AddPermission("system.cpu").Build();
RegisterExtension(extension);
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
ScriptContext* script_context = CreateScriptContext(
context, extension.get(), Feature::BLESSED_EXTENSION_CONTEXT);
script_context->set_url(extension->url());
bindings_system()->UpdateBindingsForContext(script_context);
// The system.cpu object should exist, but system.network should not (as the
// extension didn't request permission to it).
v8::Local<v8::Value> system_cpu =
V8ValueFromScriptSource(context, "chrome.system.cpu");
ASSERT_FALSE(system_cpu.IsEmpty());
EXPECT_TRUE(system_cpu->IsObject());
EXPECT_FALSE(system_cpu->IsUndefined());
v8::Local<v8::Value> system_network =
V8ValueFromScriptSource(context, "chrome.system.network");
ASSERT_FALSE(system_network.IsEmpty());
EXPECT_TRUE(system_network->IsUndefined());
const char kUseSystemCpu[] =
"(function() {\n"
" chrome.system.cpu.getInfo(function() {})\n"
"});";
v8::Local<v8::Function> use_system_cpu =
FunctionFromString(context, kUseSystemCpu);
RunFunctionOnGlobal(use_system_cpu, context, 0, nullptr);
EXPECT_EQ(extension->id(), last_params().extension_id);
EXPECT_EQ("system.cpu.getInfo", last_params().name);
EXPECT_TRUE(last_params().has_callback);
}
TEST_F(NativeExtensionBindingsSystemUnittest, TestLastError) {
scoped_refptr<const Extension> extension =
ExtensionBuilder("foo").AddPermissions({"idle", "power"}).Build();
RegisterExtension(extension);
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
ScriptContext* script_context = CreateScriptContext(
context, extension.get(), Feature::BLESSED_EXTENSION_CONTEXT);
script_context->set_url(extension->url());
bindings_system()->UpdateBindingsForContext(script_context);
const char kCallFunction[] =
"(function() {\n"
" chrome.idle.queryState(30, function(state) {\n"
" if (chrome.runtime.lastError)\n"
" this.lastErrorMessage = chrome.runtime.lastError.message;\n"
" });\n"
"});";
v8::Local<v8::Function> function = FunctionFromString(context, kCallFunction);
ASSERT_FALSE(function.IsEmpty());
RunFunctionOnGlobal(function, context, 0, nullptr);
// Validate the params that would be sent to the browser.
EXPECT_EQ(extension->id(), last_params().extension_id);
EXPECT_EQ("idle.queryState", last_params().name);
int first_request_id = last_params().request_id;
// Respond with an error.
bindings_system()->HandleResponse(last_params().request_id, false,
base::ListValue(), "Some API Error");
EXPECT_EQ("\"Some API Error\"",
GetStringPropertyFromObject(context->Global(), context,
"lastErrorMessage"));
// Test responding with a failure, but no set error.
RunFunctionOnGlobal(function, context, 0, nullptr);
EXPECT_EQ(extension->id(), last_params().extension_id);
EXPECT_EQ("idle.queryState", last_params().name);
EXPECT_NE(first_request_id, last_params().request_id);
bindings_system()->HandleResponse(last_params().request_id, false,
base::ListValue(), std::string());
EXPECT_EQ("\"Unknown error.\"",
GetStringPropertyFromObject(context->Global(), context,
"lastErrorMessage"));
}
TEST_F(NativeExtensionBindingsSystemUnittest, TestCustomProperties) {
scoped_refptr<const Extension> extension =
ExtensionBuilder("storage extension").AddPermission("storage").Build();
RegisterExtension(extension);
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
ScriptContext* script_context = CreateScriptContext(
context, extension.get(), Feature::BLESSED_EXTENSION_CONTEXT);
script_context->set_url(extension->url());
bindings_system()->UpdateBindingsForContext(script_context);
v8::Local<v8::Value> storage =
V8ValueFromScriptSource(context, "chrome.storage");
ASSERT_FALSE(storage.IsEmpty());
ASSERT_TRUE(storage->IsObject());
v8::Local<v8::Value> local =
GetPropertyFromObject(storage.As<v8::Object>(), context, "local");
ASSERT_FALSE(local.IsEmpty());
ASSERT_TRUE(local->IsObject());
v8::Local<v8::Object> local_object = local.As<v8::Object>();
const std::vector<std::string> kKeys = {"get", "set", "remove", "clear",
"getBytesInUse"};
for (const auto& key : kKeys) {
v8::Local<v8::String> v8_key = gin::StringToV8(isolate(), key);
EXPECT_TRUE(local_object->HasOwnProperty(context, v8_key).FromJust())
<< key;
}
}
// Ensure that different contexts have different API objects.
TEST_F(NativeExtensionBindingsSystemUnittest,
CheckDifferentContextsHaveDifferentAPIObjects) {
scoped_refptr<const Extension> extension =
ExtensionBuilder("extension").AddPermission("idle").Build();
RegisterExtension(extension);
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context_a = MainContext();
v8::Local<v8::Context> context_b = AddContext();
ScriptContext* script_context_a = CreateScriptContext(
context_a, extension.get(), Feature::BLESSED_EXTENSION_CONTEXT);
script_context_a->set_url(extension->url());
bindings_system()->UpdateBindingsForContext(script_context_a);
ScriptContext* script_context_b = CreateScriptContext(
context_b, extension.get(), Feature::BLESSED_EXTENSION_CONTEXT);
script_context_b->set_url(extension->url());
bindings_system()->UpdateBindingsForContext(script_context_b);
auto check_properties_inequal = [](v8::Local<v8::Context> context_a,
v8::Local<v8::Context> context_b,
base::StringPiece property) {
v8::Local<v8::Value> value_a = V8ValueFromScriptSource(context_a, property);
v8::Local<v8::Value> value_b = V8ValueFromScriptSource(context_b, property);
EXPECT_FALSE(value_a.IsEmpty()) << property;
EXPECT_FALSE(value_b.IsEmpty()) << property;
EXPECT_NE(value_a, value_b) << property;
};
check_properties_inequal(context_a, context_b, "chrome");
check_properties_inequal(context_a, context_b, "chrome.idle");
check_properties_inequal(context_a, context_b, "chrome.idle.onStateChanged");
}
// Tests that API methods and events that are conditionally available based on
// context are properly present or absent from the API object.
TEST_F(NativeExtensionBindingsSystemUnittest,
CheckRestrictedFeaturesBasedOnContext) {
scoped_refptr<const Extension> connectable_extension;
{
DictionaryBuilder manifest;
manifest.Set("name", "connectable")
.Set("manifest_version", 2)
.Set("version", "0.1")
.Set("description", "test extension");
DictionaryBuilder connectable;
connectable.Set("matches",
ListBuilder().Append("*://example.com/*").Build());
manifest.Set("externally_connectable", connectable.Build());
connectable_extension =
ExtensionBuilder()
.SetManifest(manifest.Build())
.SetLocation(Manifest::INTERNAL)
.SetID(crx_file::id_util::GenerateId("connectable"))
.Build();
}
RegisterExtension(connectable_extension);
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> blessed_context = MainContext();
v8::Local<v8::Context> connectable_webpage_context = AddContext();
v8::Local<v8::Context> nonconnectable_webpage_context = AddContext();
// Create two contexts - a blessed extension context and a normal web page
// context.
ScriptContext* blessed_script_context =
CreateScriptContext(blessed_context, connectable_extension.get(),
Feature::BLESSED_EXTENSION_CONTEXT);
blessed_script_context->set_url(connectable_extension->url());
bindings_system()->UpdateBindingsForContext(blessed_script_context);
ScriptContext* connectable_webpage_script_context = CreateScriptContext(
connectable_webpage_context, nullptr, Feature::WEB_PAGE_CONTEXT);
connectable_webpage_script_context->set_url(GURL("http://example.com"));
bindings_system()->UpdateBindingsForContext(
connectable_webpage_script_context);
ScriptContext* nonconnectable_webpage_script_context = CreateScriptContext(
nonconnectable_webpage_context, nullptr, Feature::WEB_PAGE_CONTEXT);
nonconnectable_webpage_script_context->set_url(GURL("http://notexample.com"));
bindings_system()->UpdateBindingsForContext(
nonconnectable_webpage_script_context);
// Check that properties are correctly restricted. The blessed context should
// have access to the whole runtime API, the connectable webpage should only
// have access to sendMessage, and the nonconnectable webpage should not have
// access to any of the API.
const char kRuntime[] = "chrome.runtime";
const char kSendMessage[] = "chrome.runtime.sendMessage";
const char kGetUrl[] = "chrome.runtime.getURL";
const char kOnMessage[] = "chrome.runtime.onMessage";
ASSERT_TRUE(PropertyExists(blessed_context, kRuntime));
EXPECT_TRUE(PropertyExists(blessed_context, kSendMessage));
EXPECT_TRUE(PropertyExists(blessed_context, kGetUrl));
EXPECT_TRUE(PropertyExists(blessed_context, kOnMessage));
ASSERT_TRUE(PropertyExists(connectable_webpage_context, kRuntime));
EXPECT_TRUE(PropertyExists(connectable_webpage_context, kSendMessage));
EXPECT_FALSE(PropertyExists(connectable_webpage_context, kGetUrl));
EXPECT_FALSE(PropertyExists(connectable_webpage_context, kOnMessage));
EXPECT_FALSE(PropertyExists(nonconnectable_webpage_context, kRuntime));
}
// Tests behavior when script sets window.chrome to be various things.
TEST_F(NativeExtensionBindingsSystemUnittest, TestUsingOtherChromeObjects) {
scoped_refptr<const Extension> extension =
ExtensionBuilder("extension").Build();
RegisterExtension(extension);
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context_a = MainContext();
v8::Local<v8::Context> context_b = AddContext();
ScriptContext* script_context_a = CreateScriptContext(
context_a, extension.get(), Feature::BLESSED_EXTENSION_CONTEXT);
script_context_a->set_url(extension->url());
ScriptContext* script_context_b = CreateScriptContext(
context_b, extension.get(), Feature::BLESSED_EXTENSION_CONTEXT);
script_context_b->set_url(extension->url());
auto check_runtime = [this, context_a, context_b, script_context_a,
script_context_b](bool expect_b_has_runtime) {
bindings_system()->UpdateBindingsForContext(script_context_a);
bindings_system()->UpdateBindingsForContext(script_context_b);
const char kRuntime[] = "chrome.runtime";
// chrome.runtime should always exist in context a - we only mess with
// context b.
EXPECT_TRUE(PropertyExists(context_a, kRuntime));
EXPECT_EQ(expect_b_has_runtime, PropertyExists(context_b, kRuntime));
};
// By default, runtime should exist in both contexts (since both have access
// to the API).
check_runtime(true);
{
v8::Context::Scope scope(context_a);
v8::Local<v8::Object> fake_chrome = v8::Object::New(isolate());
EXPECT_EQ(context_a, fake_chrome->CreationContext());
context_b->Global()
->Set(context_b, gin::StringToSymbol(isolate(), "chrome"), fake_chrome)
.ToChecked();
}
// context_b has a chrome object that was created in a different context
// (context_a), so we shouldn't have used it. This can legitimately happen in
// the case of a parent frame modifying a child frame's window.chrome.
check_runtime(false);
{
v8::Context::Scope scope(context_b);
v8::Local<v8::Object> fake_chrome = v8::Object::New(isolate());
EXPECT_EQ(context_b, fake_chrome->CreationContext());
context_b->Global()
->Set(context_b, gin::StringToSymbol(isolate(), "chrome"), fake_chrome)
.ToChecked();
}
// When the chrome object is created in the same context (context_b), that
// object will be used.
check_runtime(true);
{
v8::Context::Scope scope(context_b);
v8::Local<v8::Boolean> fake_chrome = v8::Boolean::New(isolate(), true);
context_b->Global()
->Set(context_b, gin::StringToSymbol(isolate(), "chrome"), fake_chrome)
.ToChecked();
}
// A non-object chrome shouldn't be used.
check_runtime(false);
}
// Tests updating a context's bindings after adding or removing permissions.
TEST_F(NativeExtensionBindingsSystemUnittest, TestUpdatingPermissions) {
scoped_refptr<const Extension> extension =
ExtensionBuilder("extension").AddPermission("idle").Build();
RegisterExtension(extension);
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
ScriptContext* script_context = CreateScriptContext(
context, extension.get(), Feature::BLESSED_EXTENSION_CONTEXT);
script_context->set_url(extension->url());
bindings_system()->UpdateBindingsForContext(script_context);
// To start, chrome.idle should be available.
v8::Local<v8::Value> initial_idle =
V8ValueFromScriptSource(context, "chrome.idle");
ASSERT_FALSE(initial_idle.IsEmpty());
EXPECT_TRUE(initial_idle->IsObject());
{
// chrome.power should not be defined.
v8::Local<v8::Value> power =
V8ValueFromScriptSource(context, "chrome.power");
ASSERT_FALSE(power.IsEmpty());
EXPECT_TRUE(power->IsUndefined());
}
// Remove all permissions (`idle`).
extension->permissions_data()->SetPermissions(
std::make_unique<PermissionSet>(), std::make_unique<PermissionSet>());
bindings_system()->OnExtensionPermissionsUpdated(extension->id());
bindings_system()->UpdateBindingsForContext(script_context);
{
// TODO(devlin): Neither the native nor JS bindings systems clear the
// property on the chrome object when an API is no longer available. This
// seems unexpected, but warrants further investigation before changing
// behavior. It can be complicated by the fact that chrome.idle may not be
// the same chrome.idle the system instantiated, or may have additional
// properties.
// v8::Local<v8::Value> idle =
// V8ValueFromScriptSource(context, "chrome.idle");
// ASSERT_FALSE(idle.IsEmpty());
// EXPECT_TRUE(idle->IsUndefined());
// chrome.power should still be undefined.
v8::Local<v8::Value> power =
V8ValueFromScriptSource(context, "chrome.power");
ASSERT_FALSE(power.IsEmpty());
EXPECT_TRUE(power->IsUndefined());
}
v8::Local<v8::Function> run_idle = FunctionFromString(
context, "(function(idle) { idle.queryState(30, function() {}); })");
{
// Trying to run a chrome.idle function should fail.
v8::Local<v8::Value> args[] = {initial_idle};
RunFunctionAndExpectError(
run_idle, context, base::size(args), args,
"Uncaught Error: 'idle.queryState' is not available in this context.");
EXPECT_FALSE(has_last_params());
}
{
// Add back the `idle` permission, and also add `power`.
APIPermissionSet apis;
apis.insert(APIPermission::kPower);
apis.insert(APIPermission::kIdle);
extension->permissions_data()->SetPermissions(
std::make_unique<PermissionSet>(std::move(apis),
ManifestPermissionSet(),
URLPatternSet(), URLPatternSet()),
std::make_unique<PermissionSet>());
bindings_system()->OnExtensionPermissionsUpdated(extension->id());
bindings_system()->UpdateBindingsForContext(script_context);
}
{
// Both chrome.idle and chrome.power should be defined.
v8::Local<v8::Value> idle = V8ValueFromScriptSource(context, "chrome.idle");
ASSERT_FALSE(idle.IsEmpty());
EXPECT_TRUE(idle->IsObject());
v8::Local<v8::Value> power =
V8ValueFromScriptSource(context, "chrome.power");
ASSERT_FALSE(power.IsEmpty());
EXPECT_TRUE(power->IsObject());
}
{
// Trying to run a chrome.idle function should now succeed.
v8::Local<v8::Value> args[] = {initial_idle};
RunFunction(run_idle, context, base::size(args), args);
EXPECT_EQ("idle.queryState", last_params().name);
}
}
TEST_F(NativeExtensionBindingsSystemUnittest, UnmanagedEvents) {
scoped_refptr<const Extension> extension =
ExtensionBuilder("extension").Build();
RegisterExtension(extension);
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
ScriptContext* script_context = CreateScriptContext(
context, extension.get(), Feature::BLESSED_EXTENSION_CONTEXT);
script_context->set_url(extension->url());
bindings_system()->UpdateBindingsForContext(script_context);
const char kAddListeners[] =
"(function() {\n"
" chrome.runtime.onMessage.addListener(function() {});\n"
" chrome.runtime.onConnect.addListener(function() {});\n"
"});";
v8::Local<v8::Function> add_listeners =
FunctionFromString(context, kAddListeners);
RunFunctionOnGlobal(add_listeners, context, 0, nullptr);
// We should have no notifications for event listeners added (since the
// mock is a strict mock, this will fail if anything was called).
::testing::Mock::VerifyAndClearExpectations(ipc_message_sender());
}
// Tests that a context having access to an aliased API (like networking.onc)
// does not allow for accessing the source API (networkingPrivate) directly.
TEST_F(NativeExtensionBindingsSystemUnittest,
AccessToAliasSourceDoesntGiveAliasAccess) {
const char kWhitelistedId[] = "pkedcjkdefgpdelpbcmbmeomcjbeemfm";
scoped_refptr<const Extension> extension =
ExtensionBuilder("extension")
.SetID(kWhitelistedId)
.AddPermission("networkingPrivate")
.Build();
RegisterExtension(extension);
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
ScriptContext* script_context = CreateScriptContext(
context, extension.get(), Feature::BLESSED_EXTENSION_CONTEXT);
bindings_system()->UpdateBindingsForContext(script_context);
// The extension only has access to networkingPrivate, so networking.onc
// (and chrome.networking in general) should be undefined.
EXPECT_EQ("object",
gin::V8ToString(isolate(),
V8ValueFromScriptSource(
context, "typeof chrome.networkingPrivate")));
EXPECT_EQ(
"undefined",
gin::V8ToString(isolate(), V8ValueFromScriptSource(
context, "typeof chrome.networking")));
}
// Tests that a context having access to the source for an aliased API does not
// allow for accessing the alias.
TEST_F(NativeExtensionBindingsSystemUnittest,
AccessToAliasDoesntGiveAliasSourceAccess) {
const char kWhitelistedId[] = "pkedcjkdefgpdelpbcmbmeomcjbeemfm";
scoped_refptr<const Extension> extension =
ExtensionBuilder("extension")
.SetID(kWhitelistedId)
.AddPermission("networking.onc")
.Build();
RegisterExtension(extension);
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
ScriptContext* script_context = CreateScriptContext(
context, extension.get(), Feature::BLESSED_EXTENSION_CONTEXT);
bindings_system()->UpdateBindingsForContext(script_context);
// The extension only has access to networking.onc, so networkingPrivate
// should be undefined.
EXPECT_EQ("undefined",
gin::V8ToString(isolate(),
V8ValueFromScriptSource(
context, "typeof chrome.networkingPrivate")));
EXPECT_EQ(
"object",
gin::V8ToString(isolate(), V8ValueFromScriptSource(
context, "typeof chrome.networking.onc")));
}
// Test that if an extension has access to both an alias and an alias source,
// the objects on the API are different.
TEST_F(NativeExtensionBindingsSystemUnittest, AliasedAPIsAreDifferentObjects) {
const char kWhitelistedId[] = "pkedcjkdefgpdelpbcmbmeomcjbeemfm";
scoped_refptr<const Extension> extension =
ExtensionBuilder("extension")
.SetID(kWhitelistedId)
.AddPermissions({"networkingPrivate", "networking.onc"})
.Build();
RegisterExtension(extension);
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
ScriptContext* script_context = CreateScriptContext(
context, extension.get(), Feature::BLESSED_EXTENSION_CONTEXT);
bindings_system()->UpdateBindingsForContext(script_context);
// Both APIs should be defined, since the extension has access to each.
EXPECT_EQ("object",
gin::V8ToString(isolate(),
V8ValueFromScriptSource(
context, "typeof chrome.networkingPrivate")));
EXPECT_EQ(
"object",
gin::V8ToString(isolate(), V8ValueFromScriptSource(
context, "typeof chrome.networking.onc")));
// The APIs should not be equal.
bool equal = true;
EXPECT_TRUE(gin::ConvertFromV8(
isolate(),
V8ValueFromScriptSource(
context, "chrome.networkingPrivate == chrome.networking.onc"),
&equal));
EXPECT_FALSE(equal);
}
// Tests that script can overwrite the value of an API.
TEST_F(NativeExtensionBindingsSystemUnittest, CanOverwriteAPIs) {
scoped_refptr<const Extension> extension =
ExtensionBuilder("extension").Build();
RegisterExtension(extension);
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
ScriptContext* script_context = CreateScriptContext(
context, extension.get(), Feature::BLESSED_EXTENSION_CONTEXT);
script_context->set_url(extension->url());
bindings_system()->UpdateBindingsForContext(script_context);
v8::Local<v8::Function> overwrite_api =
FunctionFromString(context, "(function() { chrome.runtime = 'bar'; })");
RunFunction(overwrite_api, context, 0, nullptr);
v8::Local<v8::Value> property =
V8ValueFromScriptSource(context, "chrome.runtime");
EXPECT_TRUE(property->IsString());
EXPECT_EQ("bar", gin::V8ToString(isolate(), property));
}
// Tests that script can delete an API property.
TEST_F(NativeExtensionBindingsSystemUnittest, CanDeleteAPIs) {
scoped_refptr<const Extension> extension =
ExtensionBuilder("extension").Build();
RegisterExtension(extension);
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
ScriptContext* script_context = CreateScriptContext(
context, extension.get(), Feature::BLESSED_EXTENSION_CONTEXT);
script_context->set_url(extension->url());
bindings_system()->UpdateBindingsForContext(script_context);
v8::Local<v8::Object> chrome =
GetPropertyFromObject(context->Global(), context, "chrome")
.As<v8::Object>();
v8::Local<v8::String> runtime_key = gin::StringToSymbol(isolate(), "runtime");
{
v8::Maybe<bool> has_runtime = chrome->HasOwnProperty(context, runtime_key);
ASSERT_TRUE(has_runtime.IsJust());
EXPECT_TRUE(has_runtime.FromJust());
}
v8::Local<v8::Function> delete_api =
FunctionFromString(context, "(function() { delete chrome.runtime; })");
RunFunction(delete_api, context, 0, nullptr);
{
v8::Maybe<bool> has_runtime = chrome->HasOwnProperty(context, runtime_key);
ASSERT_TRUE(has_runtime.IsJust());
EXPECT_FALSE(has_runtime.FromJust());
}
v8::Local<v8::Value> property =
V8ValueFromScriptSource(context, "chrome.runtime");
EXPECT_TRUE(property->IsUndefined());
}
// Test that API initialization happens in the owning context.
TEST_F(NativeExtensionBindingsSystemUnittest, APIIsInitializedByOwningContext) {
// Attach custom JS hooks.
const char kCustomBinding[] =
R"(this.apiBridge = apiBridge;
apiBridge.registerCustomHook(() => {});)";
source_map()->RegisterModule("idle", kCustomBinding);
scoped_refptr<const Extension> extension =
ExtensionBuilder("foo").AddPermission("idle").Build();
RegisterExtension(extension);
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
ScriptContext* script_context = CreateScriptContext(
context, extension.get(), Feature::BLESSED_EXTENSION_CONTEXT);
script_context->set_url(extension->url());
bindings_system()->UpdateBindingsForContext(script_context);
{
// Create a second, uninitialized context, which will trigger the
// construction of chrome.idle in the first context.
set_allow_unregistered_contexts(true);
v8::Local<v8::Context> second_context = AddContext();
v8::Local<v8::Function> get_idle = FunctionFromString(
second_context, "(function(chrome) { chrome.idle; })");
v8::Local<v8::Value> chrome =
context->Global()
->Get(context, gin::StringToV8(isolate(), "chrome"))
.ToLocalChecked();
ASSERT_TRUE(chrome->IsObject());
v8::Context::Scope context_scope(second_context);
v8::Local<v8::Value> args[] = {chrome};
RunFunction(get_idle, second_context, base::size(args), args);
}
// The apiBridge should have been created in the owning (original) context,
// even though the initialization was triggered by the second context.
v8::Local<v8::Value> api_bridge =
context->Global()
->Get(context, gin::StringToV8(isolate(), "apiBridge"))
.ToLocalChecked();
ASSERT_TRUE(api_bridge->IsObject());
EXPECT_EQ(context, api_bridge.As<v8::Object>()->CreationContext());
}
class ResponseValidationNativeExtensionBindingsSystemUnittest
: public NativeExtensionBindingsSystemUnittest,
public testing::WithParamInterface<bool> {
public:
ResponseValidationNativeExtensionBindingsSystemUnittest() = default;
~ResponseValidationNativeExtensionBindingsSystemUnittest() override = default;
void SetUp() override {
response_validation_override_ =
binding::SetResponseValidationEnabledForTesting(GetParam());
NativeExtensionBindingsSystemUnittest::SetUp();
}
void TearDown() override {
NativeExtensionBindingsSystemUnittest::TearDown();
response_validation_override_.reset();
}
private:
std::unique_ptr<base::AutoReset<bool>> response_validation_override_;
DISALLOW_COPY_AND_ASSIGN(
ResponseValidationNativeExtensionBindingsSystemUnittest);
};
TEST_P(ResponseValidationNativeExtensionBindingsSystemUnittest,
ResponseValidation) {
// The APIResponseValidator should only be used if response validation is
// enabled. Otherwise, it should be null.
EXPECT_EQ(GetParam(), bindings_system()
->api_system()
->request_handler()
->has_response_validator_for_testing());
base::Optional<std::string> validation_failure_method_name;
base::Optional<std::string> validation_failure_error;
auto on_validation_failure =
[&validation_failure_method_name, &validation_failure_error](
const std::string& method_name, const std::string& error) {
validation_failure_method_name = method_name;
validation_failure_error = error;
};
APIResponseValidator::TestHandler test_validation_failure_handler(
base::BindLambdaForTesting(on_validation_failure));
scoped_refptr<const Extension> extension =
ExtensionBuilder("foo")
.AddPermissions({"idle", "power", "webRequest"})
.Build();
RegisterExtension(extension);
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
ScriptContext* script_context = CreateScriptContext(
context, extension.get(), Feature::BLESSED_EXTENSION_CONTEXT);
script_context->set_url(extension->url());
bindings_system()->UpdateBindingsForContext(script_context);
const char kCallIdleQueryState[] =
"(function() { chrome.idle.queryState(30, function() {}); })";
v8::Local<v8::Function> call_idle_query_state =
FunctionFromString(context, kCallIdleQueryState);
RunFunctionOnGlobal(call_idle_query_state, context, 0, nullptr);
EXPECT_FALSE(validation_failure_method_name);
EXPECT_FALSE(validation_failure_error);
// Respond with a valid value. Validation should not fail.
ASSERT_TRUE(has_last_params());
bindings_system()->HandleResponse(last_params().request_id, true,
*ListValueFromString("['active']"),
std::string());
EXPECT_FALSE(validation_failure_method_name);
EXPECT_FALSE(validation_failure_error);
// Run the function again, and response with an invalid value.
RunFunctionOnGlobal(call_idle_query_state, context, 0, nullptr);
ASSERT_TRUE(has_last_params());
bindings_system()->HandleResponse(last_params().request_id, true,
*ListValueFromString("['bad enum']"),
std::string());
// Validation should fail iff response validation is enabled.
if (GetParam()) {
EXPECT_EQ("idle.queryState",
validation_failure_method_name.value_or("no value"));
EXPECT_EQ(api_errors::ArgumentError(
"newState",
api_errors::InvalidEnumValue({"active", "idle", "locked"})),
validation_failure_error.value_or("no value"));
} else {
EXPECT_FALSE(validation_failure_method_name);
EXPECT_FALSE(validation_failure_error);
}
}
INSTANTIATE_TEST_SUITE_P(
,
ResponseValidationNativeExtensionBindingsSystemUnittest,
testing::Bool());
} // namespace extensions