Chromium Code Reviews| Index: extensions/renderer/js_extension_bindings_system.cc |
| diff --git a/extensions/renderer/js_extension_bindings_system.cc b/extensions/renderer/js_extension_bindings_system.cc |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..081605aa0a6f2e009e3e17ac8ea98c74d02b891d |
| --- /dev/null |
| +++ b/extensions/renderer/js_extension_bindings_system.cc |
| @@ -0,0 +1,311 @@ |
| +// 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 "content/public/child/v8_value_converter.h" |
| +#include "content/public/common/content_switches.h" |
| +#include "extensions/common/extension.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/renderer_extension_registry.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 { |
| + |
| +static const char kEventDispatchFunction[] = "dispatchEvent"; |
| + |
| +// 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, |
| + v8::Isolate* isolate) { |
| + v8::Local<v8::String> key = v8::String::NewFromUtf8(isolate, field.c_str()); |
| + // 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(key)) { |
| + object->Delete(key); |
| + } else if (object->HasRealNamedProperty(key)) { |
| + v8::Local<v8::Value> value = object->Get(key); |
| + CHECK(value->IsObject()); |
| + return v8::Local<v8::Object>::Cast(value); |
| + } |
| + |
| + v8::Local<v8::Object> new_object = v8::Object::New(isolate); |
| + object->Set(key, new_object); |
| + 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::Local<v8::Object> global(context->v8_context()->Global()); |
| + v8::Local<v8::Value> chrome(global->Get(chrome_string)); |
| + if (chrome->IsUndefined()) { |
| + chrome = v8::Object::New(context->isolate()); |
| + global->Set(chrome_string, chrome); |
| + } |
| + 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->isolate()); |
| + } |
| + |
| + if (only_ancestor_available) |
| + return v8::Local<v8::Object>(); |
| + |
| + if (bind_name) |
|
lazyboy
2016/11/19 03:23:01
Is |bind_name| ever null?
Devlin
2016/11/21 19:29:41
Nope, removed.
|
| + *bind_name = split.back(); |
| + |
| + return bind_object.IsEmpty() ? GetOrCreateChrome(context) : bind_object; |
| +} |
| + |
| +// Determines if a ScriptContext can connect to any externally_connectable- |
| +// enabled extension. |
| +bool IsRuntimeAvailableToContext(ScriptContext* context) { |
| + for (const auto& extension : |
| + *RendererExtensionRegistry::Get()->GetMainThreadExtensionSet()) { |
| + ExternallyConnectableInfo* info = static_cast<ExternallyConnectableInfo*>( |
| + extension->GetManifestData(manifest_keys::kExternallyConnectable)); |
| + if (info && info->matches.MatchesURL(context->url())) |
| + return true; |
| + } |
| + return false; |
| +} |
| + |
| +// 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<RequestSender> request_sender) |
| + : source_map_(source_map), request_sender_(std::move(request_sender)) {} |
| + |
| +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) { |
| + 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. |
| + if (context->GetAvailability("app").is_available()) |
| + RegisterBinding("app", context); |
| + if (context->GetAvailability("webstore").is_available()) |
| + RegisterBinding("webstore", context); |
| + if (context->GetAvailability("dashboardPrivate").is_available()) |
| + RegisterBinding("dashboardPrivate", context); |
| + if (IsRuntimeAvailableToContext(context)) |
| + RegisterBinding("runtime", context); |
| + break; |
| + |
| + case Feature::SERVICE_WORKER_CONTEXT: |
| + DCHECK(ExtensionsClient::Get() |
| + ->ExtensionAPIEnabledInExtensionServiceWorkers()); |
| + // Intentional fallthrough. |
| + case Feature::BLESSED_EXTENSION_CONTEXT: |
| + case Feature::UNBLESSED_EXTENSION_CONTEXT: |
| + case Feature::CONTENT_SCRIPT_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.get()) != 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)) { |
| + // TODO(lazyboy): RegisterBinding() uses |source_map_|, any thread |
| + // safety issue? |
| + RegisterBinding(map_entry.first, context); |
| + } |
| + } |
| + break; |
| + } |
| + } |
| +} |
| + |
| +void JsExtensionBindingsSystem::HandleResponse(int request_id, |
| + bool success, |
| + const base::ListValue& response, |
| + const std::string& error) { |
| + request_sender_->HandleResponse(request_id, success, response, error); |
| +} |
| + |
| +RequestSender* JsExtensionBindingsSystem::GetRequestSender() { |
| + return request_sender_.get(); |
| +} |
| + |
| +void JsExtensionBindingsSystem::DispatchEventInContext( |
| + const std::string& event_name, |
| + const base::ListValue* event_args, |
| + const base::DictionaryValue* filtering_info, |
| + ScriptContext* context) { |
| + v8::HandleScope handle_scope(context->isolate()); |
| + v8::Context::Scope context_scope(context->v8_context()); |
| + |
| + std::vector<v8::Local<v8::Value>> arguments; |
| + arguments.push_back(gin::StringToSymbol(context->isolate(), event_name)); |
| + |
| + { |
| + std::unique_ptr<content::V8ValueConverter> converter( |
| + content::V8ValueConverter::create()); |
| + arguments.push_back( |
| + converter->ToV8Value(event_args, context->v8_context())); |
| + if (!filtering_info->empty()) { |
| + arguments.push_back( |
| + converter->ToV8Value(filtering_info, context->v8_context())); |
| + } |
| + } |
| + |
| + context->module_system()->CallModuleMethodSafe( |
| + kEventBindings, kEventDispatchFunction, arguments.size(), |
| + arguments.data()); |
| +} |
| + |
| +void JsExtensionBindingsSystem::RegisterBinding(const std::string& api_name, |
| + ScriptContext* context) { |
| + std::string bind_name; |
| + v8::Local<v8::Object> bind_object = |
| + GetOrCreateBindObjectIfAvailable(api_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()); |
| + if (bind_object->HasRealNamedProperty(v8_bind_name)) { |
| + // 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(v8_bind_name)) |
| + return; // lazy binding still there, nothing to do |
| + if (bind_object->Get(v8_bind_name)->IsObject()) |
| + return; // binding has already been fully installed |
| + } |
| + |
| + ModuleSystem* module_system = context->module_system(); |
| + if (!source_map_->Contains(api_name)) { |
| + module_system->RegisterNativeHandler( |
| + api_name, |
| + std::unique_ptr<NativeHandler>( |
| + new BindingGeneratingNativeHandler(context, api_name, "binding"))); |
| + module_system->SetNativeLazyField(bind_object, bind_name, api_name, |
| + "binding"); |
| + } else { |
| + module_system->SetLazyField(bind_object, bind_name, api_name, "binding"); |
| + } |
| +} |
| + |
| +} // namespace extensions |