| // 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/bindings/event_emitter.h" |
| |
| #include <algorithm> |
| |
| #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 kEventEmitterTypeName[] = "Event"; |
| |
| } // namespace |
| |
| gin::WrapperInfo EventEmitter::kWrapperInfo = {gin::kEmbedderNativeGin}; |
| |
| 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() {} |
| |
| gin::ObjectTemplateBuilder EventEmitter::GetObjectTemplateBuilder( |
| v8::Isolate* isolate) { |
| return 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::GetTypeName() { |
| return kEventEmitterTypeName; |
| } |
| |
| void EventEmitter::Fire(v8::Local<v8::Context> context, |
| std::vector<v8::Local<v8::Value>>* args, |
| const EventFilteringInfo* filter, |
| JSRunner::ResultCallback callback) { |
| DispatchAsync(context, args, filter, std::move(callback)); |
| } |
| |
| v8::Local<v8::Value> EventEmitter::FireSync( |
| v8::Local<v8::Context> context, |
| std::vector<v8::Local<v8::Value>>* args, |
| const EventFilteringInfo* filter) { |
| DCHECK(context == context->GetIsolate()->GetCurrentContext()); |
| return DispatchSync(context, args, filter); |
| } |
| |
| void EventEmitter::Invalidate(v8::Local<v8::Context> context) { |
| valid_ = false; |
| listeners_->Invalidate(context); |
| } |
| |
| size_t EventEmitter::GetNumListeners() const { |
| return listeners_->GetNumListeners(); |
| } |
| |
| 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::Context> context = arguments->GetHolderCreationContext(); |
| if (!gin::PerContextData::From(context)) |
| return; |
| |
| std::string error; |
| if (!listeners_->AddListener(listener, filter, 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); |
| } |
| |
| bool EventEmitter::HasListeners() { |
| return listeners_->GetNumListeners() != 0; |
| } |
| |
| void EventEmitter::Dispatch(gin::Arguments* arguments) { |
| if (!valid_) |
| return; |
| |
| if (listeners_->GetNumListeners() == 0) |
| return; |
| |
| v8::Isolate* isolate = arguments->isolate(); |
| v8::HandleScope handle_scope(isolate); |
| v8::Local<v8::Context> context = isolate->GetCurrentContext(); |
| std::vector<v8::Local<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, |
| std::vector<v8::Local<v8::Value>>* args, |
| const EventFilteringInfo* filter) { |
| // Note that |listeners_| can be modified during handling. |
| std::vector<v8::Local<v8::Function>> listeners = |
| listeners_->GetListeners(filter, context); |
| |
| JSRunner* js_runner = JSRunner::Get(context); |
| v8::Isolate* isolate = context->GetIsolate(); |
| 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->size(), args->data()); |
| |
| // 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()); |
| exception_handler_->HandleException(context, "Error in event handler", |
| &try_catch); |
| 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, |
| std::vector<v8::Local<v8::Value>>* args, |
| const EventFilteringInfo* filter, |
| JSRunner::ResultCallback callback) { |
| v8::Isolate* isolate = context->GetIsolate(); |
| v8::HandleScope handle_scope(isolate); |
| v8::Context::Scope context_scope(context); |
| |
| // 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_.emplace(filter_id, *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()); |
| } |
| |
| 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)) |
| .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, 0, nullptr, |
| std::move(callback)); |
| } |
| |
| // 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(); |
| base::Optional<EventFilteringInfo> filter; |
| if (filter_id != kInvalidFilterId) { |
| auto filter_iter = emitter->pending_filters_.find(filter_id); |
| DCHECK(filter_iter != emitter->pending_filters_.end()); |
| filter = std::move(filter_iter->second); |
| emitter->pending_filters_.erase(filter_iter); |
| } |
| |
| 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>(); |
| std::vector<v8::Local<v8::Value>> arguments; |
| 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()); |
| |
| // We know that dispatching synchronously should be safe because this function |
| // was triggered by JS execution. |
| info.GetReturnValue().Set(emitter->DispatchSync( |
| context, &arguments, filter ? &filter.value() : nullptr)); |
| } |
| |
| } // namespace extensions |