910e62b5创建于 1月15日历史提交
// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "extensions/renderer/bindings/api_binding_hooks.h"

#include "base/debug/dump_without_crashing.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/stringprintf.h"
#include "base/supports_user_data.h"
#include "extensions/renderer/bindings/api_binding_hooks_delegate.h"
#include "extensions/renderer/bindings/api_binding_util.h"
#include "extensions/renderer/bindings/api_request_handler.h"
#include "extensions/renderer/bindings/api_signature.h"
#include "extensions/renderer/bindings/js_runner.h"
#include "gin/arguments.h"
#include "gin/data_object_builder.h"
#include "gin/object_template_builder.h"
#include "gin/per_context_data.h"
#include "gin/public/wrappable_pointer_tags.h"
#include "gin/wrappable.h"
#include "v8/include/cppgc/allocation.h"
#include "v8/include/v8-cppgc.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<JSHookInterface> {
 public:
  static constexpr gin::WrapperInfo kWrapperInfo = {
      {gin::kEmbedderNativeGin},
      gin::kJSHookInterface};

  const gin::WrapperInfo* wrapper_info() const override { return &kWrapperInfo; }

  explicit JSHookInterface(const std::string& api_name)
      : api_name_(api_name) {}

  JSHookInterface(const JSHookInterface&) = delete;
  JSHookInterface& operator=(const JSHookInterface&) = delete;

  // gin::Wrappable:
  gin::ObjectTemplateBuilder GetObjectTemplateBuilder(
      v8::Isolate* isolate) final {
    return gin::Wrappable<JSHookInterface>::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<v8::Function> GetHandleRequestHook(const std::string& method_name,
                                               v8::Isolate* isolate) const {
    return GetHookFromMap(handle_request_hooks_, method_name, isolate);
  }

  v8::Local<v8::Function> GetPreValidationHook(const std::string& method_name,
                                               v8::Isolate* isolate) const {
    return GetHookFromMap(pre_validation_hooks_, method_name, isolate);
  }

  v8::Local<v8::Function> GetPostValidationHook(const std::string& method_name,
                                                v8::Isolate* isolate) const {
    return GetHookFromMap(post_validation_hooks_, method_name, isolate);
  }

  v8::Local<v8::Function> GetCustomCallback(const std::string& method_name,
                                            v8::Isolate* isolate) const {
    return GetHookFromMap(custom_callback_hooks_, method_name, isolate);
  }

 private:
  using JSHooks = std::map<std::string, v8::Global<v8::Function>>;

  v8::Local<v8::Function> 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<v8::Function>();
    return iter->second.Get(isolate);
  }

  void AddHookToMap(JSHooks* map,
                    v8::Isolate* isolate,
                    const std::string& method_name,
                    v8::Local<v8::Function> hook) {
    std::string qualified_method_name =
        base::StringPrintf("%s.%s", api_name_.c_str(), method_name.c_str());
    v8::Global<v8::Function>& entry = (*map)[qualified_method_name];
    if (!entry.IsEmpty()) {
      NOTREACHED() << "Hooks can only be set once.";
    }
    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<v8::Function> 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<v8::Function> hook) {
    AddHookToMap(&pre_validation_hooks_, isolate, method_name, hook);
  }

  void SetUpdateArgumentsPostValidate(v8::Isolate* isolate,
                                      const std::string& method_name,
                                      v8::Local<v8::Function> hook) {
    AddHookToMap(&post_validation_hooks_, isolate, method_name, hook);
  }

  void SetCustomCallback(v8::Isolate* isolate,
                         const std::string& method_name,
                         v8::Local<v8::Function> 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_;
};

const char kExtensionAPIHooksPerContextKey[] = "extension_api_hooks";

struct APIHooksPerContextData : public base::SupportsUserData::Data {
  explicit 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<JSHookInterface*>::FromV8(
          isolate, pair.second.Get(isolate), &hooks);
      CHECK(hooks);
      hooks->ClearHooks();
    }
  }

  raw_ptr<v8::Isolate> isolate;

  std::map<std::string, v8::Global<v8::Object>> hook_interfaces;

  // For handle request hooks which need to be resolved asynchronously, we store
  // a map of the associated request IDs to the callbacks that will be used to
  // resolve them.
  using ActiveRequest = base::OnceCallback<void(bool, gin::Arguments*)>;
  std::map<int, ActiveRequest> active_requests;
};

// Gets the v8::Object of the JSHookInterface, optionally creating it if it
// doesn't exist.
v8::Local<v8::Object> GetJSHookInterfaceObject(
    const std::string& api_name,
    v8::Local<v8::Context> context,
    bool should_create) {
  gin::PerContextData* per_context_data = gin::PerContextData::From(context);
  DCHECK(per_context_data);
  APIHooksPerContextData* data = static_cast<APIHooksPerContextData*>(
      per_context_data->GetUserData(kExtensionAPIHooksPerContextKey));
  if (!data) {
    if (!should_create)
      return v8::Local<v8::Object>();

    auto api_data =
        std::make_unique<APIHooksPerContextData>(v8::Isolate::GetCurrent());
    data = api_data.get();
    per_context_data->SetUserData(kExtensionAPIHooksPerContextKey,
                                  std::move(api_data));
  }

  auto iter = data->hook_interfaces.find(api_name);
  if (iter != data->hook_interfaces.end())
    return iter->second.Get(v8::Isolate::GetCurrent());

  if (!should_create)
    return v8::Local<v8::Object>();

  v8::Isolate* isolate = v8::Isolate::GetCurrent();
  auto* hooks = cppgc::MakeGarbageCollected<JSHookInterface>(
      isolate->GetCppHeap()->GetAllocationHandle(), api_name);
  v8::Local<v8::Object> hooks_object =
      hooks->GetWrapper(isolate).ToLocalChecked();
  data->hook_interfaces[api_name].Reset(isolate, hooks_object);

  return hooks_object;
}

// Helper function used when completing requests for handle request hooks that
// had an associated asynchronous response expected.
void CompleteHandleRequestHelper(
    const v8::FunctionCallbackInfo<v8::Value>& info,
    bool did_succeed) {
  gin::Arguments args(info);
  v8::Local<v8::Context> context = args.isolate()->GetCurrentContext();
  if (!binding::IsContextValid(context))
    return;
  int request_id = 0;
  bool got_request_id = args.GetData(&request_id);
  DCHECK(got_request_id);

  // The callback to complete the request is stored in a map on the
  // APIHooksPerContextData associated with the id of the request.
  gin::PerContextData* per_context_data = gin::PerContextData::From(context);
  DCHECK(per_context_data);
  APIHooksPerContextData* data = static_cast<APIHooksPerContextData*>(
      per_context_data->GetUserData(kExtensionAPIHooksPerContextKey));
  DCHECK(data) << "APIHooks PerContextData should always exist if we have an "
                  "active request";

  auto iter = data->active_requests.find(request_id);
  if (iter == data->active_requests.end()) {
    // In theory there should always be an associated stored request found, but
    // if one of our custom bindings erroneously calls the callbacks for
    // completing a request more than once the associated request will have
    // already been removed. If that is the case we bail early.
    // TODO(tjudkins): Audit existing handle request custom hooks to see if this
    // could happen in any of them. crbug.com/1298409 seemed to indicate this
    // was happening, hence why we fail gracefully here to avoid a crash.
    LOG(ERROR) << "No callback found for the specified request ID.";
    base::debug::DumpWithoutCrashing();
    return;
  }
  auto callback = std::move(iter->second);
  data->active_requests.erase(iter);
  std::move(callback).Run(did_succeed, &args);
}

// Helper function to add a success and failure callback to the arguments passed
// to handle request hooks that require an asynchronous response and add a
// pending request to handle resolving it. Updates |arguments| to replace the
// trailing callback with a custom handler function to resolve the request on a
// success and adds another handler function to the end of |arguments| for
// resolving in the case of a failure. Also adds the associated promise to the
// return on |result| if this is for a promise based request.
void AddSuccessAndFailureCallbacks(
    v8::Local<v8::Context> context,
    binding::AsyncResponseType async_type,
    APIRequestHandler& request_handler,
    binding::ResultModifierFunction result_modifier,
    base::WeakPtr<APIBindingHooks> weak_ptr,
    v8::LocalVector<v8::Value>* arguments,
    APIBindingHooks::RequestResult& result) {
  DCHECK(!arguments->empty());

  // Since ParseArgumentsToV8 fills missing optional arguments with null, the
  // final argument should either be a function if the API was called with a
  // callback or null if it was left off.
  // Note: the response callback here can actually remain empty in the case
  // of an optional callback being left off in a context that doesn't support
  // promises.
  v8::Local<v8::Function> response_callback;
  if (async_type == binding::AsyncResponseType::kCallback) {
    DCHECK(arguments->back()->IsFunction());
    response_callback = arguments->back().As<v8::Function>();
  } else if (async_type == binding::AsyncResponseType::kPromise) {
    DCHECK(arguments->back()->IsNull());
  }

  APIRequestHandler::RequestDetails request_details =
      request_handler.AddPendingRequest(context, async_type, response_callback,
                                        std::move(result_modifier));
  DCHECK_EQ(async_type == binding::AsyncResponseType::kPromise,
            !request_details.promise.IsEmpty());
  result.return_value = request_details.promise;

  // We store the callbacks to complete the requests in a map on the
  // APIHooksPerContextData associated with the request id.
  v8::Local<v8::Value> v8_request_id =
      v8::Integer::New(v8::Isolate::GetCurrent(), request_details.request_id);
  gin::PerContextData* per_context_data = gin::PerContextData::From(context);
  DCHECK(per_context_data);
  APIHooksPerContextData* data = static_cast<APIHooksPerContextData*>(
      per_context_data->GetUserData(kExtensionAPIHooksPerContextKey));
  DCHECK(data) << "APIHooks PerContextData should always exist if we have an "
                  "active request";
  data->active_requests.emplace(
      request_details.request_id,
      base::BindOnce(&APIBindingHooks::CompleteHandleRequest,
                     std::move(weak_ptr), request_details.request_id));

  v8::Local<v8::Function> success_callback =
      v8::Function::New(
          context,
          [](const v8::FunctionCallbackInfo<v8::Value>& info) {
            CompleteHandleRequestHelper(info, true);
          },
          v8_request_id)
          .ToLocalChecked();
  v8::Local<v8::Function> failure_callback =
      v8::Function::New(
          context,
          [](const v8::FunctionCallbackInfo<v8::Value>& info) {
            CompleteHandleRequestHelper(info, false);
          },
          v8_request_id)
          .ToLocalChecked();
  // The success callback replaces any existing callback that may have
  // been at the end of the arguments and the failure callback is appended
  // to the end.
  arguments->back() = success_callback;
  arguments->push_back(failure_callback);
}

}  // namespace

APIBindingHooks::RequestResult::RequestResult(ResultCode code) : code(code) {}
APIBindingHooks::RequestResult::RequestResult(
    ResultCode code,
    v8::Local<v8::Function> custom_callback)
    : code(code), custom_callback(custom_callback) {}
APIBindingHooks::RequestResult::RequestResult(
    ResultCode code,
    v8::Local<v8::Function> custom_callback,
    binding::ResultModifierFunction result_modifier)
    : code(code),
      custom_callback(custom_callback),
      result_modifier(std::move(result_modifier)) {}
APIBindingHooks::RequestResult::RequestResult(std::string invocation_error)
    : code(INVALID_INVOCATION), error(std::move(invocation_error)) {}
APIBindingHooks::RequestResult::~RequestResult() = default;
APIBindingHooks::RequestResult::RequestResult(RequestResult&& other) = default;

APIBindingHooks::APIBindingHooks(const std::string& api_name,
                                 APIRequestHandler* request_handler)
    : api_name_(api_name), request_handler_(request_handler) {}
APIBindingHooks::~APIBindingHooks() = default;

APIBindingHooks::RequestResult APIBindingHooks::RunHooks(
    const std::string& method_name,
    v8::Local<v8::Context> context,
    const APISignature* signature,
    v8::LocalVector<v8::Value>* arguments,
    const APITypeReferenceMap& type_refs) {
  binding::ResultModifierFunction result_modifier;
  // Easy case: a native custom hook.
  if (delegate_) {
    RequestResult result = delegate_->HandleRequest(
        method_name, signature, context, arguments, type_refs);
    // If the native hooks handled the call, set a custom callback or a result
    // modifier, use that.
    if (result.code != RequestResult::NOT_HANDLED ||
        !result.custom_callback.IsEmpty()) {
      return result;
    }
    // If the native hooks didn't handle the call but did set a result modifier,
    // grab it to be able to use it along with the custom hooks below.
    result_modifier = std::move(result.result_modifier);
  }

  // Harder case: looking up a custom hook registered on the context (since
  // these are JS, each context has a separate instance).
  v8::Local<v8::Object> hook_interface_object =
      GetJSHookInterfaceObject(api_name_, context, false);
  if (hook_interface_object.IsEmpty()) {
    return RequestResult(RequestResult::NOT_HANDLED, v8::Local<v8::Function>(),
                         std::move(result_modifier));
  }

  v8::Isolate* isolate = v8::Isolate::GetCurrent();

  JSHookInterface* hook_interface = nullptr;
  gin::Converter<JSHookInterface*>::FromV8(
      isolate,
      hook_interface_object, &hook_interface);
  CHECK(hook_interface);

  v8::Local<v8::Function> 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 (!binding::IsContextValid(context))
      return RequestResult(RequestResult::CONTEXT_INVALIDATED);

    if (try_catch.HasCaught()) {
      try_catch.ReThrow();
      return RequestResult(RequestResult::THROWN);
    }
  }

  v8::Local<v8::Function> post_validate_hook =
      hook_interface->GetPostValidationHook(method_name, isolate);
  v8::Local<v8::Function> handle_request =
      hook_interface->GetHandleRequestHook(method_name, isolate);
  v8::Local<v8::Function> 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,
                         std::move(result_modifier));
  }

  // ... otherwise, we have to validate the arguments.
  APISignature::V8ParseResult parse_result =
      signature->ParseArgumentsToV8(context, *arguments, type_refs);

  if (!binding::IsContextValid(context))
    return RequestResult(RequestResult::CONTEXT_INVALIDATED);

  if (try_catch.HasCaught()) {
    try_catch.ReThrow();
    return RequestResult(RequestResult::THROWN);
  }
  if (!parse_result.succeeded())
    return RequestResult(std::move(*parse_result.error));
  arguments->swap(*parse_result.arguments);

  bool updated_args = false;
  if (!post_validate_hook.IsEmpty()) {
    updated_args = true;
    UpdateArguments(post_validate_hook, context, arguments);

    if (!binding::IsContextValid(context))
      return RequestResult(RequestResult::CONTEXT_INVALIDATED);

    if (try_catch.HasCaught()) {
      try_catch.ReThrow();
      return RequestResult(RequestResult::THROWN);
    }
  }

  if (handle_request.IsEmpty()) {
    RequestResult::ResultCode result = updated_args
                                           ? RequestResult::ARGUMENTS_UPDATED
                                           : RequestResult::NOT_HANDLED;
    return RequestResult(result, custom_callback, std::move(result_modifier));
  }

  RequestResult result(RequestResult::HANDLED, custom_callback);

  if (signature->has_async_return()) {
    AddSuccessAndFailureCallbacks(context, parse_result.async_type,
                                  *request_handler_, std::move(result_modifier),
                                  weak_factory_.GetWeakPtr(), arguments,
                                  result);
  }

  // Safe to use synchronous JS since it's in direct response to JS calling
  // into the binding.
  v8::MaybeLocal<v8::Value> v8_result =
      JSRunner::Get(context)->RunJSFunctionSync(handle_request, context,
                                                *arguments);

  if (!binding::IsContextValid(context))
    return RequestResult(RequestResult::CONTEXT_INVALIDATED);

  if (try_catch.HasCaught()) {
    try_catch.ReThrow();
    return RequestResult(RequestResult::THROWN);
  }

  if (!v8_result.ToLocalChecked()->IsUndefined()) {
    DCHECK(result.return_value.IsEmpty())
        << "A handleRequest hook cannot return a synchronous result from an "
           "API that supports promises.";
    result.return_value = v8_result.ToLocalChecked();
  }
  return result;
}

void APIBindingHooks::CompleteHandleRequest(int request_id,
                                            bool did_succeed,
                                            gin::Arguments* arguments) {
  if (did_succeed) {
    request_handler_->CompleteRequest(request_id, arguments->GetAll(),
                                      /*error*/ std::string());
  } else {
    CHECK_EQ(arguments->Length(), 1);
    v8::Local<v8::Value> error = arguments->GetAll()[0];
    DCHECK(error->IsString());

    // In the case of an error we don't respond with any arguments.
    v8::LocalVector<v8::Value> response_list(arguments->isolate());
    request_handler_->CompleteRequest(
        request_id, response_list,
        gin::V8ToString(arguments->isolate(), error));
  }
}

v8::Local<v8::Object> APIBindingHooks::GetJSHookInterface(
    v8::Local<v8::Context> context) {
  return GetJSHookInterfaceObject(api_name_, context, true);
}

bool APIBindingHooks::CreateCustomEvent(v8::Local<v8::Context> context,
                                        const std::string& event_name,
                                        v8::Local<v8::Value>* event_out) {
  return delegate_ &&
         delegate_->CreateCustomEvent(context, event_name, event_out);
}

void APIBindingHooks::InitializeTemplate(
    v8::Isolate* isolate,
    v8::Local<v8::ObjectTemplate> object_template,
    const APITypeReferenceMap& type_refs) {
  if (delegate_)
    delegate_->InitializeTemplate(isolate, object_template, type_refs);
}

void APIBindingHooks::InitializeInstance(v8::Local<v8::Context> context,
                                         v8::Local<v8::Object> instance) {
  if (delegate_)
    delegate_->InitializeInstance(context, instance);
}

void APIBindingHooks::SetDelegate(
    std::unique_ptr<APIBindingHooksDelegate> delegate) {
  delegate_ = std::move(delegate);
}

bool APIBindingHooks::UpdateArguments(v8::Local<v8::Function> function,
                                      v8::Local<v8::Context> context,
                                      v8::LocalVector<v8::Value>* arguments) {
  v8::Local<v8::Value> result;
  v8::Isolate* isolate = v8::Isolate::GetCurrent();
  {
    v8::TryCatch try_catch(isolate);
    // Safe to use synchronous JS since it's in direct response to JS calling
    // into the binding.
    v8::MaybeLocal<v8::Value> maybe_result =
        JSRunner::Get(context)->RunJSFunctionSync(function, context,
                                                  *arguments);
    if (try_catch.HasCaught()) {
      try_catch.ReThrow();
      return false;
    }
    result = maybe_result.ToLocalChecked();
  }
  v8::LocalVector<v8::Value> new_args(isolate);
  if (result.IsEmpty() || !gin::Converter<v8::LocalVector<v8::Value>>::FromV8(
                              isolate, result, &new_args)) {
    return false;
  }
  arguments->swap(new_args);
  return true;
}

}  // namespace extensions