blob: 78d870b26f68605b8451e86468d5fb691ce56a8e [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/js_extension_bindings_system.h"
#include "base/command_line.h"
#include "base/strings/string_split.h"
#include "base/timer/elapsed_timer.h"
#include "content/public/common/content_switches.h"
#include "content/public/renderer/v8_value_converter.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_api.h"
#include "extensions/common/extension_urls.h"
#include "extensions/common/extensions_client.h"
#include "extensions/common/features/feature.h"
#include "extensions/common/features/feature_provider.h"
#include "extensions/common/manifest_constants.h"
#include "extensions/common/manifest_handlers/externally_connectable.h"
#include "extensions/renderer/binding_generating_native_handler.h"
#include "extensions/renderer/event_bindings.h"
#include "extensions/renderer/event_bookkeeper.h"
#include "extensions/renderer/ipc_message_sender.h"
#include "extensions/renderer/renderer_extension_registry.h"
#include "extensions/renderer/request_sender.h"
#include "extensions/renderer/resource_bundle_source_map.h"
#include "extensions/renderer/script_context.h"
#include "gin/converter.h"
#include "v8/include/v8.h"
namespace extensions {
namespace {
// Gets |field| from |object| or creates it as an empty object if it doesn't
// exist.
v8::Local<v8::Object> GetOrCreateObject(const v8::Local<v8::Object>& object,
const std::string& field,
ScriptContext* context) {
v8::Local<v8::String> key =
v8::String::NewFromUtf8(context->isolate(), field.c_str(),
v8::NewStringType::kInternalized)
.ToLocalChecked();
// If the object has a callback property, it is assumed it is an unavailable
// API, so it is safe to delete. This is checked before GetOrCreateObject is
// called.
if (object->HasRealNamedCallbackProperty(context->v8_context(), key)
.FromMaybe(false)) {
object->Delete(context->v8_context(), key).ToChecked();
} else if (object->HasRealNamedProperty(context->v8_context(), key)
.FromMaybe(false)) {
v8::Local<v8::Value> value =
object->Get(context->v8_context(), key).ToLocalChecked();
CHECK(value->IsObject());
return v8::Local<v8::Object>::Cast(value);
}
v8::Local<v8::Object> new_object = v8::Object::New(context->isolate());
object->Set(context->v8_context(), key, new_object).ToChecked();
return new_object;
}
// Returns the global value for "chrome" from |context|. If one doesn't exist
// creates a new object for it. If a chrome property exists on the window
// already (as in the case when a script did `window.chrome = true`), returns
// an empty object.
v8::Local<v8::Object> GetOrCreateChrome(ScriptContext* context) {
v8::Local<v8::String> chrome_string(
v8::String::NewFromUtf8(context->isolate(), "chrome",
v8::NewStringType::kInternalized)
.ToLocalChecked());
v8::Local<v8::Object> global(context->v8_context()->Global());
v8::Local<v8::Value> chrome(
global->Get(context->v8_context(), chrome_string).ToLocalChecked());
if (chrome->IsUndefined()) {
chrome = v8::Object::New(context->isolate());
global->Set(context->v8_context(), chrome_string, chrome).ToChecked();
}
return chrome->IsObject() ? chrome.As<v8::Object>() : v8::Local<v8::Object>();
}
v8::Local<v8::Object> GetOrCreateBindObjectIfAvailable(
const std::string& api_name,
std::string* bind_name,
ScriptContext* context) {
std::vector<std::string> split = base::SplitString(
api_name, ".", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
v8::Local<v8::Object> bind_object;
// Check if this API has an ancestor. If the API's ancestor is available and
// the API is not available, don't install the bindings for this API. If
// the API is available and its ancestor is not, delete the ancestor and
// install the bindings for the API. This is to prevent loading the ancestor
// API schema if it will not be needed.
//
// For example:
// If app is available and app.window is not, just install app.
// If app.window is available and app is not, delete app and install
// app.window on a new object so app does not have to be loaded.
const FeatureProvider* api_feature_provider =
FeatureProvider::GetAPIFeatures();
std::string ancestor_name;
bool only_ancestor_available = false;
for (size_t i = 0; i < split.size() - 1; ++i) {
ancestor_name += (i ? "." : "") + split[i];
if (api_feature_provider->GetFeature(ancestor_name) &&
context->GetAvailability(ancestor_name).is_available() &&
!context->GetAvailability(api_name).is_available()) {
only_ancestor_available = true;
break;
}
if (bind_object.IsEmpty()) {
bind_object = GetOrCreateChrome(context);
if (bind_object.IsEmpty())
return v8::Local<v8::Object>();
}
bind_object = GetOrCreateObject(bind_object, split[i], context);
}
if (only_ancestor_available)
return v8::Local<v8::Object>();
DCHECK(bind_name);
*bind_name = split.back();
return bind_object.IsEmpty() ? GetOrCreateChrome(context) : bind_object;
}
// Creates the event bindings if necessary for the given |context|.
void MaybeCreateEventBindings(ScriptContext* context) {
// chrome.Event is part of the public API (although undocumented). Make it
// lazily evalulate to Event from event_bindings.js. For extensions only
// though, not all webpages!
if (!context->extension())
return;
v8::Local<v8::Object> chrome = GetOrCreateChrome(context);
if (chrome.IsEmpty())
return;
context->module_system()->SetLazyField(chrome, "Event", kEventBindings,
"Event");
}
} // namespace
JsExtensionBindingsSystem::JsExtensionBindingsSystem(
ResourceBundleSourceMap* source_map,
std::unique_ptr<IPCMessageSender> ipc_message_sender)
: source_map_(source_map),
ipc_message_sender_(std::move(ipc_message_sender)),
request_sender_(
std::make_unique<RequestSender>(ipc_message_sender_.get())),
messaging_service_(this) {}
JsExtensionBindingsSystem::~JsExtensionBindingsSystem() {}
void JsExtensionBindingsSystem::DidCreateScriptContext(ScriptContext* context) {
MaybeCreateEventBindings(context);
}
void JsExtensionBindingsSystem::WillReleaseScriptContext(
ScriptContext* context) {
// TODO(kalman): Make |request_sender| use |context->AddInvalidationObserver|.
// In fact |request_sender_| should really be owned by ScriptContext.
request_sender_->InvalidateSource(context);
}
void JsExtensionBindingsSystem::UpdateBindingsForContext(
ScriptContext* context) {
base::ElapsedTimer timer;
v8::HandleScope handle_scope(context->isolate());
v8::Context::Scope context_scope(context->v8_context());
// TODO(kalman): Make the bindings registration have zero overhead then run
// the same code regardless of context type.
switch (context->context_type()) {
case Feature::UNSPECIFIED_CONTEXT:
case Feature::WEB_PAGE_CONTEXT:
case Feature::BLESSED_WEB_PAGE_CONTEXT:
// Hard-code registration of any APIs that are exposed to webpage-like
// contexts, because it's too expensive to run the full bindings code.
// All of the same permission checks will still apply.
for (const char* feature_name : kWebAvailableFeatures) {
if (context->GetAvailability(feature_name).is_available())
RegisterBinding(feature_name, feature_name, context);
}
if (IsRuntimeAvailableToContext(context))
RegisterBinding("runtime", "runtime", context);
break;
case Feature::SERVICE_WORKER_CONTEXT:
DCHECK(ExtensionsClient::Get()
->ExtensionAPIEnabledInExtensionServiceWorkers());
FALLTHROUGH;
case Feature::BLESSED_EXTENSION_CONTEXT:
case Feature::UNBLESSED_EXTENSION_CONTEXT:
case Feature::CONTENT_SCRIPT_CONTEXT:
case Feature::LOCK_SCREEN_EXTENSION_CONTEXT:
case Feature::WEBUI_CONTEXT: {
// Extension context; iterate through all the APIs and bind the available
// ones.
const FeatureProvider* api_feature_provider =
FeatureProvider::GetAPIFeatures();
for (const auto& map_entry : api_feature_provider->GetAllFeatures()) {
// Internal APIs are included via require(api_name) from internal code
// rather than chrome[api_name].
if (map_entry.second->IsInternal())
continue;
// If this API has a parent feature (and isn't marked 'noparent'),
// then this must be a function or event, so we should not register.
if (api_feature_provider->GetParent(*map_entry.second) != nullptr)
continue;
// Skip chrome.test if this isn't a test.
if (map_entry.first == "test" &&
!base::CommandLine::ForCurrentProcess()->HasSwitch(
::switches::kTestType)) {
continue;
}
if (context->IsAnyFeatureAvailableToContext(
*map_entry.second, CheckAliasStatus::NOT_ALLOWED)) {
// Check if the API feature is indeed an alias. If it is, the API
// should use source API bindings as its own.
const std::string& source = map_entry.second->source();
// TODO(lazyboy): RegisterBinding() uses |source_map_|, any thread
// safety issue?
RegisterBinding(source.empty() ? map_entry.first : source,
map_entry.first, context);
}
}
break;
}
}
LogUpdateBindingsForContextTime(context->context_type(), timer.Elapsed());
}
void JsExtensionBindingsSystem::HandleResponse(int request_id,
bool success,
const base::ListValue& response,
const std::string& error) {
request_sender_->HandleResponse(request_id, success, response, error);
ipc_message_sender_->SendOnRequestResponseReceivedIPC(request_id);
}
RequestSender* JsExtensionBindingsSystem::GetRequestSender() {
return request_sender_.get();
}
IPCMessageSender* JsExtensionBindingsSystem::GetIPCMessageSender() {
return ipc_message_sender_.get();
}
RendererMessagingService* JsExtensionBindingsSystem::GetMessagingService() {
return &messaging_service_;
}
void JsExtensionBindingsSystem::DispatchEventInContext(
const std::string& event_name,
const base::ListValue* event_args,
const EventFilteringInfo* filtering_info,
ScriptContext* context) {
EventBindings::DispatchEventInContext(event_name, event_args, filtering_info,
context);
}
bool JsExtensionBindingsSystem::HasEventListenerInContext(
const std::string& event_name,
ScriptContext* context) {
return EventBookkeeper::Get()->HasListener(context, event_name);
}
void JsExtensionBindingsSystem::RegisterBinding(
const std::string& api_name,
const std::string& api_bind_name,
ScriptContext* context) {
std::string bind_name;
v8::Local<v8::Object> bind_object =
GetOrCreateBindObjectIfAvailable(api_bind_name, &bind_name, context);
// Empty if the bind object failed to be created, probably because the
// extension overrode chrome with a non-object, e.g. window.chrome = true.
if (bind_object.IsEmpty())
return;
v8::Local<v8::String> v8_bind_name =
v8::String::NewFromUtf8(context->isolate(), bind_name.c_str(),
v8::NewStringType::kInternalized)
.ToLocalChecked();
if (bind_object->HasRealNamedProperty(context->v8_context(), v8_bind_name)
.FromMaybe(false)) {
// The bind object may already have the property if the API has been
// registered before (or if the extension has put something there already,
// but, whatevs).
//
// In the former case, we need to re-register the bindings for the APIs
// which the extension now has permissions for (if any), but not touch any
// others so that we don't destroy state such as event listeners.
//
// TODO(kalman): Only register available APIs to make this all moot.
if (bind_object
->HasRealNamedCallbackProperty(context->v8_context(), v8_bind_name)
.FromMaybe(false))
return; // lazy binding still there, nothing to do
if (bind_object->Get(context->v8_context(), v8_bind_name)
.ToLocalChecked()
->IsObject())
return; // binding has already been fully installed
}
ModuleSystem* module_system = context->module_system();
if (!source_map_->Contains(api_name)) {
module_system->RegisterNativeHandler(
api_bind_name,
std::unique_ptr<NativeHandler>(
new BindingGeneratingNativeHandler(context, api_name, "binding")));
module_system->SetNativeLazyField(bind_object, bind_name, api_bind_name,
"binding");
} else {
module_system->SetLazyField(bind_object, bind_name, api_name, "binding");
}
}
} // namespace extensions