// 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/api_binding_hooks.h" #include "base/memory/ptr_util.h" #include "base/strings/stringprintf.h" #include "base/supports_user_data.h" #include "extensions/renderer/api_signature.h" #include "gin/arguments.h" #include "gin/handle.h" #include "gin/object_template_builder.h" #include "gin/per_context_data.h" #include "gin/wrappable.h" namespace extensions { namespace { // An interface to allow for registration of custom hooks from JavaScript. // Contains registered hooks for a single API. class JSHookInterface final : public gin::Wrappable { public: explicit JSHookInterface(const std::string& api_name) : api_name_(api_name) {} static gin::WrapperInfo kWrapperInfo; // gin::Wrappable: gin::ObjectTemplateBuilder GetObjectTemplateBuilder( v8::Isolate* isolate) override { return Wrappable::GetObjectTemplateBuilder(isolate) .SetMethod("setHandleRequest", &JSHookInterface::SetHandleRequest) .SetMethod("setUpdateArgumentsPreValidate", &JSHookInterface::SetUpdateArgumentsPreValidate) .SetMethod("setUpdateArgumentsPostValidate", &JSHookInterface::SetUpdateArgumentsPostValidate) .SetMethod("setCustomCallback", &JSHookInterface::SetCustomCallback); } void ClearHooks() { handle_request_hooks_.clear(); pre_validation_hooks_.clear(); post_validation_hooks_.clear(); } v8::Local GetHandleRequestHook(const std::string& method_name, v8::Isolate* isolate) const { return GetHookFromMap(handle_request_hooks_, method_name, isolate); } v8::Local GetPreValidationHook(const std::string& method_name, v8::Isolate* isolate) const { return GetHookFromMap(pre_validation_hooks_, method_name, isolate); } v8::Local GetPostValidationHook(const std::string& method_name, v8::Isolate* isolate) const { return GetHookFromMap(post_validation_hooks_, method_name, isolate); } v8::Local GetCustomCallback(const std::string& method_name, v8::Isolate* isolate) const { return GetHookFromMap(custom_callback_hooks_, method_name, isolate); } private: using JSHooks = std::map>; v8::Local GetHookFromMap(const JSHooks& map, const std::string& method_name, v8::Isolate* isolate) const { auto iter = map.find(method_name); if (iter == map.end()) return v8::Local(); return iter->second.Get(isolate); } void AddHookToMap(JSHooks* map, v8::Isolate* isolate, const std::string& method_name, v8::Local hook) { std::string qualified_method_name = base::StringPrintf("%s.%s", api_name_.c_str(), method_name.c_str()); v8::Global& entry = (*map)[qualified_method_name]; if (!entry.IsEmpty()) { NOTREACHED() << "Hooks can only be set once."; return; } entry.Reset(isolate, hook); } // Adds a hook to handle the implementation of the API method. void SetHandleRequest(v8::Isolate* isolate, const std::string& method_name, v8::Local hook) { AddHookToMap(&handle_request_hooks_, isolate, method_name, hook); } // Adds a hook to update the arguments passed to the API method before we do // any kind of validation. void SetUpdateArgumentsPreValidate(v8::Isolate* isolate, const std::string& method_name, v8::Local hook) { AddHookToMap(&pre_validation_hooks_, isolate, method_name, hook); } void SetUpdateArgumentsPostValidate(v8::Isolate* isolate, const std::string& method_name, v8::Local hook) { AddHookToMap(&post_validation_hooks_, isolate, method_name, hook); } void SetCustomCallback(v8::Isolate* isolate, const std::string& method_name, v8::Local hook) { AddHookToMap(&custom_callback_hooks_, isolate, method_name, hook); } std::string api_name_; JSHooks handle_request_hooks_; JSHooks pre_validation_hooks_; JSHooks post_validation_hooks_; JSHooks custom_callback_hooks_; DISALLOW_COPY_AND_ASSIGN(JSHookInterface); }; const char kExtensionAPIHooksPerContextKey[] = "extension_api_hooks"; struct APIHooksPerContextData : public base::SupportsUserData::Data { APIHooksPerContextData(v8::Isolate* isolate) : isolate(isolate) {} ~APIHooksPerContextData() override { v8::HandleScope scope(isolate); for (const auto& pair : hook_interfaces) { // We explicitly clear the hook data map here to remove all references to // v8 objects in order to avoid cycles. JSHookInterface* hooks = nullptr; gin::Converter::FromV8( isolate, pair.second.Get(isolate), &hooks); CHECK(hooks); hooks->ClearHooks(); } } v8::Isolate* isolate; std::map> hook_interfaces; }; gin::WrapperInfo JSHookInterface::kWrapperInfo = {gin::kEmbedderNativeGin}; // Gets the v8::Object of the JSHookInterface, optionally creating it if it // doesn't exist. v8::Local GetJSHookInterfaceObject( const std::string& api_name, v8::Local context, bool should_create) { gin::PerContextData* per_context_data = gin::PerContextData::From(context); DCHECK(per_context_data); APIHooksPerContextData* data = static_cast( per_context_data->GetUserData(kExtensionAPIHooksPerContextKey)); if (!data) { if (!should_create) return v8::Local(); auto api_data = base::MakeUnique(context->GetIsolate()); data = api_data.get(); per_context_data->SetUserData(kExtensionAPIHooksPerContextKey, api_data.release()); } auto iter = data->hook_interfaces.find(api_name); if (iter != data->hook_interfaces.end()) return iter->second.Get(context->GetIsolate()); if (!should_create) return v8::Local(); gin::Handle hooks = gin::CreateHandle(context->GetIsolate(), new JSHookInterface(api_name)); CHECK(!hooks.IsEmpty()); v8::Local hooks_object = hooks.ToV8().As(); data->hook_interfaces[api_name].Reset(context->GetIsolate(), hooks_object); return hooks_object; } } // namespace APIBindingHooks::RequestResult::RequestResult(ResultCode code) : code(code) {} APIBindingHooks::RequestResult::RequestResult( ResultCode code, v8::Local custom_callback) : code(code), custom_callback(custom_callback) {} APIBindingHooks::RequestResult::~RequestResult() {} APIBindingHooks::RequestResult::RequestResult(const RequestResult& other) = default; APIBindingHooks::APIBindingHooks(const std::string& api_name, const binding::RunJSFunctionSync& run_js) : api_name_(api_name), run_js_(run_js) {} APIBindingHooks::~APIBindingHooks() {} void APIBindingHooks::RegisterHandleRequest(const std::string& method_name, const HandleRequestHook& hook) { DCHECK(!hooks_used_) << "Hooks must be registered before the first use!"; request_hooks_[method_name] = hook; } void APIBindingHooks::RegisterJsSource(v8::Global source, v8::Global resource_name) { js_hooks_source_ = std::move(source); js_resource_name_ = std::move(resource_name); } APIBindingHooks::RequestResult APIBindingHooks::RunHooks( const std::string& method_name, v8::Local context, const APISignature* signature, std::vector>* arguments, const APITypeReferenceMap& type_refs) { // Easy case: a native custom hook. auto request_hooks_iter = request_hooks_.find(method_name); if (request_hooks_iter != request_hooks_.end()) { RequestResult result = request_hooks_iter->second.Run( signature, context, arguments, type_refs); // Right now, it doesn't make sense to register a request handler that // doesn't handle the request. DCHECK_NE(RequestResult::NOT_HANDLED, result.code); return result; } // Harder case: looking up a custom hook registered on the context (since // these are JS, each context has a separate instance). v8::Local hook_interface_object = GetJSHookInterfaceObject(api_name_, context, false); if (hook_interface_object.IsEmpty()) return RequestResult(RequestResult::NOT_HANDLED); v8::Isolate* isolate = context->GetIsolate(); JSHookInterface* hook_interface = nullptr; gin::Converter::FromV8( isolate, hook_interface_object, &hook_interface); CHECK(hook_interface); v8::Local pre_validate_hook = hook_interface->GetPreValidationHook(method_name, isolate); v8::TryCatch try_catch(isolate); if (!pre_validate_hook.IsEmpty()) { // TODO(devlin): What to do with the result of this function call? Can it // only fail in the case we've already thrown? UpdateArguments(pre_validate_hook, context, arguments); if (try_catch.HasCaught()) { try_catch.ReThrow(); return RequestResult(RequestResult::THROWN); } } v8::Local post_validate_hook = hook_interface->GetPostValidationHook(method_name, isolate); v8::Local handle_request = hook_interface->GetHandleRequestHook(method_name, isolate); v8::Local custom_callback = hook_interface->GetCustomCallback(method_name, isolate); // If both the post validation hook and the handle request hook are empty, // we're done... if (post_validate_hook.IsEmpty() && handle_request.IsEmpty()) return RequestResult(RequestResult::NOT_HANDLED, custom_callback); { // ... otherwise, we have to validate the arguments. std::vector> parsed_v8_args; std::string error; bool success = signature->ParseArgumentsToV8(context, *arguments, type_refs, &parsed_v8_args, &error); if (try_catch.HasCaught()) { try_catch.ReThrow(); return RequestResult(RequestResult::THROWN); } if (!success) return RequestResult(RequestResult::INVALID_INVOCATION); arguments->swap(parsed_v8_args); } if (!post_validate_hook.IsEmpty()) { UpdateArguments(post_validate_hook, context, arguments); if (try_catch.HasCaught()) { try_catch.ReThrow(); return RequestResult(RequestResult::THROWN); } } if (handle_request.IsEmpty()) return RequestResult(RequestResult::NOT_HANDLED, custom_callback); v8::Global global_result = run_js_.Run(handle_request, context, arguments->size(), arguments->data()); if (try_catch.HasCaught()) { try_catch.ReThrow(); return RequestResult(RequestResult::THROWN); } RequestResult result(RequestResult::HANDLED, custom_callback); if (!global_result.IsEmpty()) result.return_value = global_result.Get(isolate); return result; } void APIBindingHooks::InitializeInContext(v8::Local context) { if (js_hooks_source_.IsEmpty()) return; v8::Local source = js_hooks_source_.Get(context->GetIsolate()); v8::Local resource_name = js_resource_name_.Get(context->GetIsolate()); v8::Local script; v8::ScriptOrigin origin(resource_name); if (!v8::Script::Compile(context, source, &origin).ToLocal(&script)) return; v8::Local func_as_value = script->Run(); v8::Local function; if (!gin::ConvertFromV8(context->GetIsolate(), func_as_value, &function)) return; v8::Local api_hooks = GetJSHookInterface(context); v8::Local args[] = {api_hooks}; run_js_.Run(function, context, arraysize(args), args); } v8::Local APIBindingHooks::GetJSHookInterface( v8::Local context) { return GetJSHookInterfaceObject(api_name_, context, true); } v8::Local APIBindingHooks::GetCustomJSCallback( const std::string& name, v8::Local context) { v8::Local hooks = GetJSHookInterfaceObject(api_name_, context, false); if (hooks.IsEmpty()) return v8::Local(); JSHookInterface* hook_interface = nullptr; gin::Converter::FromV8(context->GetIsolate(), hooks, &hook_interface); CHECK(hook_interface); return hook_interface->GetCustomCallback(name, context->GetIsolate()); } bool APIBindingHooks::UpdateArguments( v8::Local function, v8::Local context, std::vector>* arguments) { v8::Global global_result; { v8::TryCatch try_catch(context->GetIsolate()); global_result = run_js_.Run(function, context, arguments->size(), arguments->data()); if (try_catch.HasCaught()) { try_catch.ReThrow(); return false; } } DCHECK(!global_result.IsEmpty()); v8::Local result = global_result.Get(context->GetIsolate()); std::vector> new_args; if (result.IsEmpty() || !gin::Converter>>::FromV8( context->GetIsolate(), result, &new_args)) { return false; } arguments->swap(new_args); return true; } } // namespace extensions