// 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/event_emitter.h"

#include <algorithm>
#include <utility>

#include "extensions/common/mojom/event_dispatcher.mojom.h"
#include "extensions/renderer/bindings/api_binding_util.h"
#include "extensions/renderer/bindings/api_event_listeners.h"
#include "extensions/renderer/bindings/exception_handler.h"
#include "gin/data_object_builder.h"
#include "gin/object_template_builder.h"
#include "gin/per_context_data.h"

namespace extensions {

namespace {

constexpr const char kEmitterKey[] = "emitter";
constexpr const char kArgumentsKey[] = "arguments";
constexpr const char kFilterKey[] = "filter";
constexpr const char kOnDispatchedCallbackFunctionKey[] =
    "on_dispatched_callback";
constexpr const char kListenerErrorCallbackFunctionKey[] =
    "listener_error_callback";
constexpr const char kEventEmitterTypeName[] = "Event";

constexpr const char kWebRequestEventPrefix[] = "webRequest.";
constexpr const char kWebViewInternalEventPrefix[] = "webViewInternal.";

}  // namespace

EventEmitter::EventEmitter(bool supports_filters,
                           std::unique_ptr<APIEventListeners> listeners,
                           ExceptionHandler* exception_handler)
    : supports_filters_(supports_filters),
      listeners_(std::move(listeners)),
      exception_handler_(exception_handler) {}

EventEmitter::~EventEmitter() = default;

void EventEmitter::Dispose() {
  pending_filters_.clear();
  listeners_.reset();
}

gin::ObjectTemplateBuilder EventEmitter::GetObjectTemplateBuilder(
    v8::Isolate* isolate) {
  return gin::Wrappable<EventEmitter>::GetObjectTemplateBuilder(isolate)
      .SetMethod("addListener", &EventEmitter::AddListener)
      .SetMethod("removeListener", &EventEmitter::RemoveListener)
      .SetMethod("hasListener", &EventEmitter::HasListener)
      .SetMethod("hasListeners", &EventEmitter::HasListeners)
      // The following methods aren't part of the public API, but are used
      // by our custom bindings and exposed on the public event object. :(
      // TODO(devlin): Once we convert all custom bindings that use these,
      // they can be removed.
      .SetMethod("dispatch", &EventEmitter::Dispatch);
}

const char* EventEmitter::GetHumanReadableName() const {
  return kEventEmitterTypeName;
}

void EventEmitter::Fire(v8::Local<v8::Context> context,
                        v8::LocalVector<v8::Value>* args,
                        mojom::EventFilteringInfoPtr filter,
                        v8::Local<v8::Function> on_dispatched_callback,
                        v8::Local<v8::Function> listener_error_callback) {
  DispatchAsync(context, args, std::move(filter), on_dispatched_callback,
                listener_error_callback);
}

v8::Local<v8::Value> EventEmitter::FireSync(
    v8::Local<v8::Context> context,
    v8::LocalVector<v8::Value>* args,
    mojom::EventFilteringInfoPtr filter) {
  DCHECK(context == v8::Isolate::GetCurrent()->GetCurrentContext());
  return DispatchSync(context, args, std::move(filter));
}

void EventEmitter::Invalidate(v8::Local<v8::Context> context) {
  valid_ = false;
  listeners_->Invalidate(context);
}

bool EventEmitter::HasListeners() const {
  return listeners_->GetNumListeners() != 0;
}

size_t EventEmitter::GetNumListenersForTesting() const {
  return listeners_->GetNumListeners();
}

size_t EventEmitter::GetNumPendingFiltersForTesting() const {
  return pending_filters_.size();
}

int EventEmitter::PushFilter(mojom::EventFilteringInfoPtr filter) {
  // In order to dispatch (potentially) asynchronously (such as when script is
  // suspended), use a helper function to run once JS is allowed to run,
  // currying in the necessary information about the arguments and filter.
  // We do this (rather than simply queuing up each listener and running them
  // asynchronously) for a few reasons:
  // - It allows us to catch exceptions when the listener is running.
  // - Listeners could be removed between the time the event is received and the
  //   listeners are notified.
  // - It allows us to group the listeners responses.

  // We always set a filter id (rather than leaving filter undefined in the
  // case of no filter being present) to avoid ever hitting the Object prototype
  // chain when checking for it on the data value in DispatchAsyncHelper().
  int filter_id = kInvalidFilterId;

  if (filter) {
    filter_id = next_filter_id_++;
    pending_filters_[filter_id] = std::move(filter);
  }

  return filter_id;
}

mojom::EventFilteringInfoPtr EventEmitter::PopFilter(int filter_id) {
  mojom::EventFilteringInfoPtr filter;

  if (filter_id == kInvalidFilterId) {
    return filter;
  }

  auto filter_iter = pending_filters_.find(filter_id);
  CHECK(filter_iter != pending_filters_.end());
  filter = std::move(filter_iter->second);
  pending_filters_.erase(filter_iter);

  return filter;
}

void EventEmitter::AddListener(gin::Arguments* arguments) {
  // If script from another context maintains a reference to this object, it's
  // possible that functions can be called after this object's owning context
  // is torn down and released by blink. We don't support this behavior, but
  // we need to make sure nothing crashes, so early out of methods.
  if (!valid_) {
    return;
  }

  v8::Local<v8::Function> listener;
  // TODO(devlin): For some reason, we don't throw an error when someone calls
  // add/removeListener with no argument. We probably should. For now, keep
  // the status quo, but we should revisit this.
  if (!arguments->GetNext(&listener)) {
    return;
  }

  if (!arguments->PeekNext().IsEmpty() && !supports_filters_) {
    arguments->ThrowTypeError("This event does not support filters");
    return;
  }

  v8::Local<v8::Object> filter;
  if (!arguments->PeekNext().IsEmpty() && !arguments->GetNext(&filter)) {
    arguments->ThrowTypeError("Invalid invocation");
    return;
  }

  v8::Local<v8::Object> options;
  if (!arguments->PeekNext().IsEmpty()) {
    // The `options` argument is currently limited to webRequest API only.
    std::string_view event_name = listeners_->GetEventName();
    if (!event_name.starts_with(kWebRequestEventPrefix) &&
        !event_name.starts_with(kWebViewInternalEventPrefix)) {
      arguments->ThrowTypeError("This event does not support options");
      return;
    }
    if (!arguments->GetNext(&options)) {
      arguments->ThrowTypeError("Invalid invocation");
      return;
    }
  }

  v8::Local<v8::Context> context = arguments->GetHolderCreationContext();
  if (!gin::PerContextData::From(context)) {
    return;
  }

  std::string error;
  if (!listeners_->AddListener(listener, filter, options, context, &error) &&
      !error.empty()) {
    arguments->ThrowTypeError(error);
  }
}

void EventEmitter::RemoveListener(gin::Arguments* arguments) {
  // See comment in AddListener().
  if (!valid_) {
    return;
  }

  v8::Local<v8::Function> listener;
  // See comment in AddListener().
  if (!arguments->GetNext(&listener)) {
    return;
  }

  listeners_->RemoveListener(listener, arguments->GetHolderCreationContext());
}

bool EventEmitter::HasListener(v8::Local<v8::Function> listener) {
  return listeners_->HasListener(listener);
}

void EventEmitter::Dispatch(gin::Arguments* arguments) {
  if (!valid_) {
    return;
  }

  if (!HasListeners()) {
    return;
  }

  v8::Isolate* isolate = arguments->isolate();
  v8::HandleScope handle_scope(isolate);
  v8::Local<v8::Context> context = isolate->GetCurrentContext();
  v8::LocalVector<v8::Value> v8_args = arguments->GetAll();

  // Since this is directly from JS, we know it should be safe to call
  // synchronously and use the return result, so we don't use Fire().
  arguments->Return(DispatchSync(context, &v8_args, nullptr));
}

v8::Local<v8::Value> EventEmitter::DispatchSync(
    v8::Local<v8::Context> context,
    v8::LocalVector<v8::Value>* args,
    mojom::EventFilteringInfoPtr filter) {
  return DispatchSync(
      context, args, std::move(filter),
      /*listener_error_callback_function=*/v8::Local<v8::Function>());
}

v8::Local<v8::Value> EventEmitter::DispatchSync(
    v8::Local<v8::Context> context,
    v8::LocalVector<v8::Value>* args,
    mojom::EventFilteringInfoPtr filter,
    v8::Local<v8::Function> listener_error_callback_function) {
  // Note that |listeners_| can be modified during handling.
  v8::LocalVector<v8::Function> listeners =
      listeners_->GetListeners(std::move(filter), context);

  JSRunner* js_runner = JSRunner::Get(context);
  v8::Isolate* isolate = v8::Isolate::GetCurrent();
  DCHECK(context == isolate->GetCurrentContext());

  // Gather results from each listener as we go along. This should only be
  // called when running synchronous script is allowed, and some callers
  // expect a return value of an array with entries for each of the results of
  // the listeners.
  // TODO(devlin): It'd be nice to refactor anything expecting a result here so
  // we don't have to have this special logic, especially since script could
  // potentially tweak the result object through prototype manipulation (which
  // also means we should never use this for security decisions).
  v8::Local<v8::Array> results = v8::Array::New(isolate);
  uint32_t results_index = 0;

  v8::TryCatch try_catch(isolate);
  for (const auto& listener : listeners) {
    // NOTE(devlin): Technically, any listener here could suspend JS execution
    // (through e.g. calling alert() or print()). That should suspend this
    // message loop as well (though a nested message loop will run). This is a
    // bit ugly, but should hopefully be safe.
    v8::MaybeLocal<v8::Value> maybe_result =
        js_runner->RunJSFunctionSync(listener, context, *args);

    // Any of the listeners could invalidate the context. If that happens,
    // bail out.
    if (!binding::IsContextValid(context)) {
      return v8::Undefined(isolate);
    }

    v8::Local<v8::Value> listener_result;
    if (maybe_result.ToLocal(&listener_result)) {
      if (!listener_result->IsUndefined()) {
        CHECK(
            results
                ->CreateDataProperty(context, results_index++, listener_result)
                .ToChecked());
      }
    } else {
      DCHECK(try_catch.HasCaught());

      // Notify API handlers each time an exception is thrown by a listener.
      if (!listener_error_callback_function.IsEmpty()) {
        v8::LocalVector<v8::Value> listener_error_callback_argument(isolate);
        listener_error_callback_argument.push_back(try_catch.Exception());
        JSRunner::Get(context)->RunJSFunctionSync(
            listener_error_callback_function, context,
            listener_error_callback_argument);
      }
      // NOTE: Keep in sync with `kEventHandlerErrorMessage` in
      // //extensions/renderer/resources/web_request_event.js.
      exception_handler_->HandleException(context, "Error in event handler",
                                          &try_catch);
      // `ExceptionHandler:HandleException()` can run arbitrary JS code (e.g. if
      // the thrown error defines a custom `stack` property getter), which might
      // invalidate and tear down `context`. In this case, bail out to avoid
      // executing remaining listeners on a destroyed context or dereferencing
      // freed per-context data. We `break` so that any previous listeners
      // return values are sent back to interested callers.
      if (!binding::IsContextValid(context)) {
        context.Clear();
        break;
      }
      try_catch.Reset();
    }
  }

  // Only return a value if there's at least one response. This is the behavior
  // of the current JS implementation.
  v8::Local<v8::Value> return_value;
  if (results_index > 0) {
    return_value = gin::DataObjectBuilder(isolate)
                       .Set("results", results.As<v8::Value>())
                       .Build();
  } else {
    return_value = v8::Undefined(isolate);
  }

  return return_value;
}

void EventEmitter::DispatchAsync(
    v8::Local<v8::Context> context,
    v8::LocalVector<v8::Value>* args,
    mojom::EventFilteringInfoPtr filter,
    v8::Local<v8::Function> on_dispatched_callback,
    v8::Local<v8::Function> listener_error_callback) {
  v8::Isolate* isolate = v8::Isolate::GetCurrent();
  v8::HandleScope handle_scope(isolate);
  v8::Context::Scope context_scope(context);

  int filter_id = PushFilter(std::move(filter));

  v8::Local<v8::Array> args_array = v8::Array::New(isolate, args->size());
  for (size_t i = 0; i < args->size(); ++i) {
    CHECK(args_array->CreateDataProperty(context, i, args->at(i)).ToChecked());
  }

  // Convert the functions to values so they can be set on the `data` object. We
  // set them to undefined if they're empty because the builder does not allow
  // empty values to be set on the `data` object.
  v8::Local<v8::Value> on_dispatched_callback_value =
      on_dispatched_callback.IsEmpty() ? v8::Undefined(isolate).As<v8::Value>()
                                       : on_dispatched_callback;
  v8::Local<v8::Value> listener_error_callback_value =
      listener_error_callback.IsEmpty() ? v8::Undefined(isolate).As<v8::Value>()
                                        : listener_error_callback;

  v8::Local<v8::Object> data =
      gin::DataObjectBuilder(isolate)
          .Set(kEmitterKey, GetWrapper(isolate).ToLocalChecked())
          .Set(kArgumentsKey, args_array.As<v8::Value>())
          .Set(kFilterKey, gin::ConvertToV8(isolate, filter_id))
          .Set(kOnDispatchedCallbackFunctionKey, on_dispatched_callback_value)
          .Set(kListenerErrorCallbackFunctionKey, listener_error_callback_value)
          .Build();
  v8::Local<v8::Function> function;
  // TODO(devlin): Function construction can fail in some weird cases (looking
  // up the "prototype" property on parents, failing to instantiate properties
  // on the function, etc). In *theory*, none of those apply here. Leave this as
  // a CHECK for now to flush out any cases.
  CHECK(v8::Function::New(context, &DispatchAsyncHelper, data)
            .ToLocal(&function));

  JSRunner::Get(context)->RunJSFunction(
      function, context, {},
      // We handle the callback via `callback_value` instead so we can pass
      // it v8 objects.
      JSRunner::ResultCallback());
}

// static
void EventEmitter::DispatchAsyncHelper(
    const v8::FunctionCallbackInfo<v8::Value>& info) {
  v8::Isolate* isolate = info.GetIsolate();
  v8::Local<v8::Context> context = isolate->GetCurrentContext();
  if (!binding::IsContextValid(context)) {
    return;
  }

  v8::Local<v8::Object> data = info.Data().As<v8::Object>();

  v8::Local<v8::Value> emitter_value =
      data->Get(context, gin::StringToSymbol(isolate, kEmitterKey))
          .ToLocalChecked();
  EventEmitter* emitter = nullptr;
  gin::Converter<EventEmitter*>::FromV8(isolate, emitter_value, &emitter);
  DCHECK(emitter);

  v8::Local<v8::Value> filter_id_value =
      data->Get(context, gin::StringToSymbol(isolate, kFilterKey))
          .ToLocalChecked();
  int filter_id = filter_id_value.As<v8::Int32>()->Value();
  mojom::EventFilteringInfoPtr filter = emitter->PopFilter(filter_id);

  v8::Local<v8::Value> arguments_value =
      data->Get(context, gin::StringToSymbol(isolate, kArgumentsKey))
          .ToLocalChecked();
  DCHECK(arguments_value->IsArray());
  v8::Local<v8::Array> arguments_array = arguments_value.As<v8::Array>();
  v8::LocalVector<v8::Value> arguments(isolate);
  uint32_t arguments_count = arguments_array->Length();
  arguments.reserve(arguments_count);
  for (uint32_t i = 0; i < arguments_count; ++i) {
    arguments.push_back(arguments_array->Get(context, i).ToLocalChecked());
  }

  v8::Local<v8::Value> listener_error_callback_value;
  if (!data->Get(context, gin::StringToSymbol(
                              isolate, kListenerErrorCallbackFunctionKey))
           .ToLocal(&listener_error_callback_value)) {
    NOTREACHED();
  }

  // Set the listener error callback function to an empty function if it wasn't
  // provided.
  v8::Local<v8::Function> listener_error_callback_function =
      listener_error_callback_value->IsUndefined()
          ? v8::Local<v8::Function>()
          : listener_error_callback_value.As<v8::Function>();

  // We know that dispatching synchronously should be safe because this function
  // was triggered by JS execution.
  v8::Local<v8::Value> dispatch_sync_result = emitter->DispatchSync(
      context, &arguments, std::move(filter), listener_error_callback_function);

  // Script context could be destroyed as a result of the above dispatch.
  if (!binding::IsContextValid(context)) {
    return;
  }

  v8::Local<v8::Value> on_dispatched_callback_value;
  if (!data->Get(context,
                 gin::StringToSymbol(isolate, kOnDispatchedCallbackFunctionKey))
           .ToLocal(&on_dispatched_callback_value)) {
    NOTREACHED();
  }

  // No on dispatched callback function provided, so do not call it.
  if (on_dispatched_callback_value->IsUndefined()) {
    return;
  }

  // There's a possibility that the function couldn've been modified to be empty
  // by arbitrary JS code after DispatchAsync() sets
  // `on_dispatched_callback_value`.
  if (on_dispatched_callback_value.IsEmpty()) {
    return;
  }

  v8::LocalVector<v8::Value> on_dispatched_callback_argument(isolate);
  on_dispatched_callback_argument.push_back(dispatch_sync_result);
  JSRunner::Get(context)->RunJSFunctionSync(
      on_dispatched_callback_value.As<v8::Function>(), context,
      on_dispatched_callback_argument);
}

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

}  // namespace extensions
