| // Copyright 2017 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/api/messaging/one_time_message_handler.h" |
| |
| #include <algorithm> |
| #include <map> |
| #include <memory> |
| #include <optional> |
| #include <vector> |
| |
| #include "base/containers/contains.h" |
| #include "base/feature_list.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/memory/raw_ref.h" |
| #include "base/supports_user_data.h" |
| #include "content/public/renderer/render_frame.h" |
| #include "extensions/common/api/messaging/message.h" |
| #include "extensions/common/api/messaging/port_id.h" |
| #include "extensions/common/extension_features.h" |
| #include "extensions/common/mojom/event_dispatcher.mojom.h" |
| #include "extensions/common/mojom/message_port.mojom-shared.h" |
| #include "extensions/renderer/api/messaging/message_target.h" |
| #include "extensions/renderer/api/messaging/messaging_util.h" |
| #include "extensions/renderer/bindings/api_binding_types.h" |
| #include "extensions/renderer/bindings/api_binding_util.h" |
| #include "extensions/renderer/bindings/api_bindings_system.h" |
| #include "extensions/renderer/bindings/api_event_handler.h" |
| #include "extensions/renderer/bindings/api_request_handler.h" |
| #include "extensions/renderer/bindings/get_per_context_data.h" |
| #include "extensions/renderer/console.h" |
| #include "extensions/renderer/gc_callback.h" |
| #include "extensions/renderer/get_script_context.h" |
| #include "extensions/renderer/ipc_message_sender.h" |
| #include "extensions/renderer/native_extension_bindings_system.h" |
| #include "extensions/renderer/script_context.h" |
| #include "gin/arguments.h" |
| #include "gin/data_object_builder.h" |
| #include "gin/dictionary.h" |
| #include "gin/handle.h" |
| #include "gin/per_context_data.h" |
| #include "v8/include/v8-container.h" |
| #include "v8/include/v8-exception.h" |
| #include "v8/include/v8-external.h" |
| #include "v8/include/v8-function-callback.h" |
| #include "v8/include/v8-function.h" |
| #include "v8/include/v8-isolate.h" |
| #include "v8/include/v8-object.h" |
| #include "v8/include/v8-persistent-handle.h" |
| #include "v8/include/v8-primitive.h" |
| |
| namespace extensions { |
| |
| namespace { |
| |
| // An opener port in the context; i.e., the caller of runtime.sendMessage. |
| struct OneTimeOpener { |
| int request_id = -1; |
| binding::AsyncResponseType async_type = binding::AsyncResponseType::kNone; |
| mojom::ChannelType channel_type; |
| }; |
| |
| // A receiver port in the context; i.e., a listener to runtime.onMessage. |
| struct OneTimeReceiver { |
| std::string event_name; |
| v8::Global<v8::Object> sender; |
| v8::Global<v8::Function> message_response_function; |
| }; |
| |
| struct OneTimeMessageContextData : public base::SupportsUserData::Data { |
| static constexpr char kPerContextDataKey[] = |
| "extension_one_time_message_context_data"; |
| |
| std::map<PortId, OneTimeOpener> openers; |
| // If the receiver is still present for `PortId`, then a response can still be |
| // sent from the listener to the message sender. Otherwise no response (or |
| // error) should be sent back to the message sender from the listener. |
| std::map<PortId, OneTimeReceiver> receivers; |
| |
| using OneTimePortCallbacks = |
| std::map<OneTimeMessageHandler::CallbackID, |
| std::unique_ptr<OneTimeMessageHandler::OneTimeMessageCallback>>; |
| |
| // Owns the pending callbacks used for message replies. A listener's v8 |
| // context may invoke a response callback asynchronously. This map keeps the |
| // callback alive until it is invoked or the connection is closed. Note: this |
| // struct is accessed by `OneTimeMessageHandler` but this collection is |
| // conceptually owned by |
| // `OneTimeMessageHandler::OneTimeMessageCallbackManager`. It is placed in |
| // this struct for simplicity since the classes are so interrelated. |
| std::map<PortId, OneTimePortCallbacks> pending_receiver_callbacks; |
| }; |
| |
| constexpr char OneTimeMessageContextData::kPerContextDataKey[]; |
| |
| bool IsMessagePolyfillSupportEnabled() { |
| return base::FeatureList::IsEnabled( |
| extensions_features::kRuntimeOnMessageWebExtensionPolyfillSupport); |
| } |
| |
| // Returns an array from the `result` object's `property_name` if it exists, |
| // otherwise returns an empty `v8::Local<v8::Array>`. |
| v8::Local<v8::Array> GetListenerResultArray(v8::Isolate* isolate, |
| v8::Local<v8::Context> context, |
| v8::Local<v8::Value> result, |
| const char* property_name) { |
| // `result` can be undefined if the context was destroyed before the |
| // listeners were run (or while they were running). |
| if (result->IsUndefined()) { |
| return v8::Local<v8::Array>(); |
| } |
| |
| // We expect results as a value with an array of results as a `property_name` |
| // property, however, since this comes from untrusted JS let's confirm this |
| // first. |
| if (!result->IsObject()) { |
| return v8::Local<v8::Array>(); |
| } |
| v8::Local<v8::Object> result_object = result.As<v8::Object>(); |
| v8::Local<v8::Value> array_value; |
| if (!result_object->Get(context, gin::StringToSymbol(isolate, property_name)) |
| .ToLocal(&array_value) || |
| !array_value->IsArray()) { |
| return v8::Local<v8::Array>(); |
| } |
| |
| return array_value.As<v8::Array>(); |
| } |
| |
| void DelayedOneTimeMessageCallbackHelper( |
| const v8::FunctionCallbackInfo<v8::Value>& info) { |
| CHECK(info.Data()->IsString()); |
| |
| gin::Arguments arguments(info); |
| v8::Isolate* isolate = arguments.isolate(); |
| v8::HandleScope handle_scope(isolate); |
| v8::Local<v8::Context> context = isolate->GetCurrentContext(); |
| |
| OneTimeMessageContextData* data = |
| GetPerContextData<OneTimeMessageContextData>(context, |
| kDontCreateIfMissing); |
| if (!data) |
| return; |
| |
| // Retrieve the CallbackID from v8 that we set when we created the callback. |
| v8::Local<v8::String> callback_id_v8_string = info.Data().As<v8::String>(); |
| std::string callback_id_string; |
| if (!gin::Converter<std::string>::FromV8(isolate, callback_id_v8_string, |
| &callback_id_string)) { |
| return; |
| } |
| std::optional<OneTimeMessageHandler::CallbackID> callback_id = |
| OneTimeMessageHandler::CallbackID::DeserializeFromString( |
| callback_id_string); |
| if (!callback_id) { |
| // Something must've changed this value unexpectedly. In any case we don't |
| // know which callback to run so don't run any callbacks. If this was |
| // intended for a pending callback that is still in |
| // `data->pending_receiver_callbacks`, it will be garbage collected later in |
| // `OnDelayedOneTimeMessageCallbackCollected()`. |
| return; |
| } |
| |
| // Search each `PortId` in `data->pending_receiver_callbacks` to see if any of |
| // them have `callback_id`. |
| OneTimeMessageContextData::OneTimePortCallbacks* port_callbacks = nullptr; |
| OneTimeMessageContextData::OneTimePortCallbacks::iterator port_callback_iter; |
| for (auto& port_entry : data->pending_receiver_callbacks) { |
| auto callback_entry = port_entry.second.find(*callback_id); |
| if (callback_entry == port_entry.second.end()) { |
| // `callback_id` is not associated with this `PortId`. |
| continue; |
| } |
| // Found the callback for this `callback_id`. There shouldn't be any |
| // duplicates so stop searching. |
| port_callbacks = &port_entry.second; |
| port_callback_iter = callback_entry; |
| break; |
| } |
| |
| // Couldn't find `callback_id` amongst the `PortId`s for this extension. |
| if (!port_callbacks) { |
| // One way this can happen is if an extension attempts to respond to a |
| // message multiple times despite us only allowing the first response to |
| // be sent back to the sender. If that happens, just return early to |
| // enforce this. |
| return; |
| } |
| |
| std::unique_ptr<OneTimeMessageHandler::OneTimeMessageCallback> callback = |
| std::move(port_callback_iter->second); |
| port_callbacks->erase(port_callback_iter); |
| std::move(*callback).Run(&arguments); |
| } |
| |
| } // namespace |
| |
| // A helper class to manage the creation and tracking of callbacks for |
| // one-time messages, such as the message response callback. |
| // |
| // This class creates `v8::Function`s that are associated to a C++ callback |
| // (`OneTimeMessageCallback`) and handles the cleanup of associated resources |
| // when the `v8::Function` is garbage collected. This allows message listeners |
| // to reply asynchronously without leaking resources. |
| // |
| // An instance of this class is held by the `OneTimeMessageHandler` and its |
| // lifetime is tied to the handler. |
| class OneTimeMessageHandler::OneTimeMessageCallbackManager { |
| public: |
| explicit OneTimeMessageCallbackManager( |
| OneTimeMessageHandler& owning_message_handler); |
| ~OneTimeMessageCallbackManager(); |
| |
| OneTimeMessageCallbackManager(const OneTimeMessageCallbackManager&) = delete; |
| OneTimeMessageCallbackManager& operator=( |
| const OneTimeMessageCallbackManager&) = delete; |
| |
| // Returns a v8 function that will call `callback` when the the function is |
| // called in v8. `callback` will be cleaned up when the returned function is |
| // garbage collected by v8. |
| v8::Local<v8::Function> CreateRespondingFunction( |
| ScriptContext& script_context, |
| const PortId& port_id, |
| std::unique_ptr<OneTimeMessageHandler::OneTimeMessageCallback> callback); |
| |
| // Returns a v8 function that will call `callback` after the sender's |
| // message is dispatched to all message listeners. `callback` will *not* be |
| // cleaned up when the returned function is garbage collected by v8 since it |
| // is expected to always be called synchronously immediately after event |
| // dispatch. |
| v8::Local<v8::Function> CreateEventDispatchFunction( |
| ScriptContext& script_context, |
| const PortId& port_id, |
| std::unique_ptr<OneTimeMessageHandler::OneTimeMessageCallback> callback); |
| |
| // Returns a v8 function that will call `callback` whenever a listener in the |
| // receiver throws a synchronous error. `callback` will *not* be cleaned up |
| // when the returned function is garbage collected by v8 since it is expected |
| // to cleaned up by either a) being called, or b) being deleted after all |
| // listeners have been dispatched to. |
| // `callback_id` is the unique ID of `callback` for later retrieval when the |
| // returned function calls `callback.` |
| v8::Local<v8::Function> CreateListenerThrowsErrorFunction( |
| ScriptContext& script_context, |
| const PortId& port_id, |
| std::unique_ptr<OneTimeMessageHandler::OneTimeMessageCallback> callback, |
| const CallbackID& callback_id); |
| |
| // Clears any pending `OneTimeMessageHandler::OneTimeMessageCallback`s that |
| // could be called for `port_id`. |
| void ClearCallbackDataForPortId(ScriptContext* script_context, |
| const PortId& port_id); |
| |
| // Deletes `port_id`'s `OneTimeMessageHandler::OneTimeMessageCallback` that is |
| // identified by `callback_id` . |
| void DeleteCallbackDataForCallbackId( |
| ScriptContext* script_context, |
| const PortId& port_id, |
| const OneTimeMessageHandler::CallbackID& callback_id); |
| |
| // Gets the number of pending callbacks for the `port_id` on the associated |
| // per context data for testing purposes. |
| int GetPendingCallbackCountForTest(ScriptContext* script_context, // IN-TEST |
| const PortId& port_id); |
| |
| private: |
| // Helper method for creating delayed callbacks that can be called as a |
| // result of message listener behavior. `cleanup_if_function_unused` true |
| // means that, if the context is still valid when the `v8::Function` that is |
| // created to call `callback` is garbage collected, we'll cleanup |
| // `callback`. |
| // If polyfill support is enabled we'll notify `OneTimeMessageHandler` if |
| // there are no more `OneTimeMessageHandler::OneTimeMessageCallback`s to |
| // collect, otherwise we'll notify of the one and only callback (message |
| // response) collection. |
| // `optional_callback_id` allows the caller to specify the |
| // `OneTimeMessageHandler::CallbackID` used to identify the callback, |
| // otherwise one will be generated for them. |
| v8::Local<v8::Function> CreateDelayedOneTimeMessageCallback( |
| ScriptContext& script_context, |
| const PortId& port_id, |
| std::unique_ptr<OneTimeMessageHandler::OneTimeMessageCallback> callback, |
| bool cleanup_if_function_unused); |
| v8::Local<v8::Function> CreateDelayedOneTimeMessageCallback( |
| ScriptContext& script_context, |
| const PortId& port_id, |
| std::unique_ptr<OneTimeMessageHandler::OneTimeMessageCallback> callback, |
| bool cleanup_if_function_unused, |
| std::optional<CallbackID> optional_callback_id); |
| |
| // Triggered when a `v8::Function` that had `cleanup_if_function_unused` set |
| // to true when it was created is no longer accessible in the context and v8 |
| // has garbage collected it. |
| // Used to clean up data that was stored for the `v8::Function` (the |
| // `OneTimeMessageHandler::OneTimeMessageCallback` it is associated with) |
| // and for closing the associated message port. `callback_id` is the ID of |
| // the associated `OneTimeMessageHandler::OneTimeMessageCallback`, needed |
| // for finding and erasing it from the OneTimeMessageContextData. |
| void OnDelayedOneTimeMessageCallbackCollected( |
| ScriptContext* script_context, |
| const PortId& port_id, |
| OneTimeMessageHandler::CallbackID callback_id); |
| |
| // The owning OneTimeMessageHandler. Outlives this object. |
| const raw_ref<OneTimeMessageHandler> message_handler_; |
| |
| base::WeakPtrFactory<OneTimeMessageHandler::OneTimeMessageCallbackManager> |
| weak_factory_{this}; |
| }; |
| |
| OneTimeMessageHandler::OneTimeMessageCallbackManager:: |
| OneTimeMessageCallbackManager(OneTimeMessageHandler& owning_message_handler) |
| : message_handler_(owning_message_handler) {} |
| OneTimeMessageHandler::OneTimeMessageCallbackManager:: |
| ~OneTimeMessageCallbackManager() = default; |
| |
| v8::Local<v8::Function> |
| OneTimeMessageHandler::OneTimeMessageCallbackManager::CreateRespondingFunction( |
| ScriptContext& script_context, |
| const PortId& port_id, |
| std::unique_ptr<OneTimeMessageHandler::OneTimeMessageCallback> callback) { |
| return CreateDelayedOneTimeMessageCallback( |
| script_context, port_id, std::move(callback), |
| /*cleanup_if_function_unused=*/true); |
| } |
| |
| v8::Local<v8::Function> OneTimeMessageHandler::OneTimeMessageCallbackManager:: |
| CreateEventDispatchFunction( |
| ScriptContext& script_context, |
| const PortId& port_id, |
| std::unique_ptr<OneTimeMessageHandler::OneTimeMessageCallback> |
| callback) { |
| return CreateDelayedOneTimeMessageCallback( |
| script_context, port_id, std::move(callback), |
| /*cleanup_if_function_unused=*/false); |
| } |
| |
| v8::Local<v8::Function> OneTimeMessageHandler::OneTimeMessageCallbackManager:: |
| CreateListenerThrowsErrorFunction( |
| ScriptContext& script_context, |
| const PortId& port_id, |
| std::unique_ptr<OneTimeMessageHandler::OneTimeMessageCallback> callback, |
| const CallbackID& callback_id) { |
| return CreateDelayedOneTimeMessageCallback( |
| script_context, port_id, std::move(callback), |
| /*cleanup_if_function_unused=*/false, callback_id); |
| } |
| |
| void OneTimeMessageHandler::OneTimeMessageCallbackManager:: |
| ClearCallbackDataForPortId(ScriptContext* script_context, |
| const PortId& port_id) { |
| OneTimeMessageContextData* data = |
| GetPerContextData<OneTimeMessageContextData>(script_context->v8_context(), |
| kDontCreateIfMissing); |
| if (!data) { |
| return; |
| } |
| |
| data->pending_receiver_callbacks.erase(port_id); |
| } |
| |
| void OneTimeMessageHandler::OneTimeMessageCallbackManager:: |
| DeleteCallbackDataForCallbackId( |
| ScriptContext* script_context, |
| const PortId& port_id, |
| const OneTimeMessageHandler::CallbackID& callback_id) { |
| OneTimeMessageContextData* data = |
| GetPerContextData<OneTimeMessageContextData>(script_context->v8_context(), |
| kDontCreateIfMissing); |
| if (!data) { |
| return; |
| } |
| |
| if (auto port_iter = data->pending_receiver_callbacks.find(port_id); |
| port_iter != data->pending_receiver_callbacks.end()) { |
| port_iter->second.erase(callback_id); |
| } |
| } |
| |
| int OneTimeMessageHandler::OneTimeMessageCallbackManager:: |
| GetPendingCallbackCountForTest(ScriptContext* script_context, |
| const PortId& port_id) { |
| v8::Isolate* isolate = script_context->isolate(); |
| v8::HandleScope handle_scope(isolate); |
| |
| OneTimeMessageContextData* data = |
| GetPerContextData<OneTimeMessageContextData>(script_context->v8_context(), |
| kDontCreateIfMissing); |
| |
| if (!data) { |
| return 0; |
| } |
| |
| if (auto port_iter = data->pending_receiver_callbacks.find(port_id); |
| port_iter != data->pending_receiver_callbacks.end()) { |
| return port_iter->second.size(); |
| } |
| |
| return 0; |
| } |
| |
| OneTimeMessageHandler::OneTimeMessageHandler( |
| NativeExtensionBindingsSystem* bindings_system) |
| : bindings_system_(bindings_system), |
| callback_manager_( |
| std::make_unique< |
| OneTimeMessageHandler::OneTimeMessageCallbackManager>(*this)) {} |
| OneTimeMessageHandler::~OneTimeMessageHandler() = default; |
| |
| bool OneTimeMessageHandler::HasPort(ScriptContext* script_context, |
| const PortId& port_id) { |
| v8::Isolate* isolate = script_context->isolate(); |
| v8::HandleScope handle_scope(isolate); |
| |
| OneTimeMessageContextData* data = |
| GetPerContextData<OneTimeMessageContextData>(script_context->v8_context(), |
| kDontCreateIfMissing); |
| if (!data) |
| return false; |
| return port_id.is_opener ? base::Contains(data->openers, port_id) |
| : base::Contains(data->receivers, port_id); |
| } |
| |
| v8::Local<v8::Promise> OneTimeMessageHandler::SendMessage( |
| ScriptContext* script_context, |
| const PortId& new_port_id, |
| const MessageTarget& target, |
| mojom::ChannelType channel_type, |
| const Message& message, |
| binding::AsyncResponseType async_type, |
| v8::Local<v8::Function> response_callback, |
| mojom::MessagePortHost* message_port_host, |
| mojo::PendingAssociatedRemote<mojom::MessagePort> message_port, |
| mojo::PendingAssociatedReceiver<mojom::MessagePortHost> |
| message_port_host_receiver) { |
| v8::Isolate* isolate = script_context->isolate(); |
| v8::EscapableHandleScope handle_scope(isolate); |
| |
| DCHECK(new_port_id.is_opener); |
| DCHECK_EQ(script_context->context_id(), new_port_id.context_id); |
| |
| OneTimeMessageContextData* data = |
| GetPerContextData<OneTimeMessageContextData>(script_context->v8_context(), |
| kCreateIfMissing); |
| DCHECK(data); |
| |
| v8::Local<v8::Promise> promise; |
| bool wants_response = async_type != binding::AsyncResponseType::kNone; |
| if (wants_response) { |
| // If this is a promise based request no callback should have been passed |
| // in. |
| if (async_type == binding::AsyncResponseType::kPromise) |
| DCHECK(response_callback.IsEmpty()); |
| |
| APIRequestHandler::RequestDetails details = |
| bindings_system_->api_system()->request_handler()->AddPendingRequest( |
| script_context->v8_context(), async_type, response_callback, |
| binding::ResultModifierFunction()); |
| OneTimeOpener& port = data->openers[new_port_id]; |
| port.request_id = details.request_id; |
| port.async_type = async_type; |
| port.channel_type = channel_type; |
| promise = details.promise; |
| DCHECK_EQ(async_type == binding::AsyncResponseType::kPromise, |
| !promise.IsEmpty()); |
| } |
| |
| IPCMessageSender* ipc_sender = bindings_system_->GetIPCMessageSender(); |
| std::string channel_name; |
| switch (channel_type) { |
| case mojom::ChannelType::kSendRequest: |
| channel_name = messaging_util::kSendRequestChannel; |
| break; |
| case mojom::ChannelType::kSendMessage: |
| channel_name = messaging_util::kSendMessageChannel; |
| break; |
| case mojom::ChannelType::kNative: |
| // Native messaging doesn't use channel names. |
| break; |
| case mojom::ChannelType::kConnect: |
| // connect() calls aren't handled by the OneTimeMessageHandler. |
| NOTREACHED(); |
| } |
| |
| ipc_sender->SendOpenMessageChannel( |
| script_context, new_port_id, target, channel_type, channel_name, |
| std::move(message_port), std::move(message_port_host_receiver)); |
| message_port_host->PostMessage(message); |
| |
| // If the sender doesn't provide a response callback, we can immediately |
| // close the channel. Note: we only do this for extension messages, not |
| // native apps. |
| // TODO(devlin): This is because of some subtle ordering in the browser side, |
| // where closing the channel after sending the message causes things to be |
| // destroyed in the wrong order. That would be nice to fix. |
| if (!wants_response && target.type != MessageTarget::NATIVE_APP) { |
| message_port_host->ClosePort(/*close_channel=*/true, |
| /*error_message=*/std::nullopt); |
| } |
| |
| return handle_scope.Escape(promise); |
| } |
| |
| void OneTimeMessageHandler::AddReceiver(ScriptContext* script_context, |
| const PortId& target_port_id, |
| v8::Local<v8::Object> sender, |
| const std::string& event_name) { |
| DCHECK(!target_port_id.is_opener); |
| DCHECK_NE(script_context->context_id(), target_port_id.context_id); |
| |
| v8::Isolate* isolate = script_context->isolate(); |
| v8::HandleScope handle_scope(isolate); |
| v8::Local<v8::Context> context = script_context->v8_context(); |
| |
| OneTimeMessageContextData* data = |
| GetPerContextData<OneTimeMessageContextData>(context, kCreateIfMissing); |
| DCHECK(data); |
| DCHECK(!base::Contains(data->receivers, target_port_id)); |
| OneTimeReceiver& receiver = data->receivers[target_port_id]; |
| receiver.sender.Reset(isolate, sender); |
| receiver.event_name = event_name; |
| } |
| |
| void OneTimeMessageHandler::AddReceiverForTesting( |
| ScriptContext* script_context, |
| const PortId& target_port_id, |
| v8::Local<v8::Object> sender, |
| const std::string& event_name, |
| mojo::PendingAssociatedRemote<mojom::MessagePort>& message_port_remote, |
| mojo::PendingAssociatedReceiver<mojom::MessagePortHost>& |
| message_port_host_receiver) { |
| AddReceiver(script_context, target_port_id, sender, event_name); |
| messaging_service()->BindPortForTesting( // IN-TEST |
| script_context, target_port_id, message_port_remote, |
| message_port_host_receiver); |
| } |
| |
| bool OneTimeMessageHandler::DeliverMessage(ScriptContext* script_context, |
| const Message& message, |
| const PortId& target_port_id) { |
| v8::Isolate* isolate = script_context->isolate(); |
| v8::HandleScope handle_scope(isolate); |
| |
| return target_port_id.is_opener |
| ? DeliverReplyToOpener(script_context, message, target_port_id) |
| : DeliverMessageToReceiver(script_context, message, |
| target_port_id); |
| } |
| |
| bool OneTimeMessageHandler::Disconnect(ScriptContext* script_context, |
| const PortId& port_id, |
| const std::string& error_message) { |
| v8::Isolate* isolate = script_context->isolate(); |
| v8::HandleScope handle_scope(isolate); |
| |
| return port_id.is_opener |
| ? DisconnectOpener(script_context, port_id, error_message) |
| : DisconnectReceiver(script_context, port_id); |
| } |
| |
| int OneTimeMessageHandler::GetPendingCallbackCountForTest( |
| ScriptContext* script_context, |
| const PortId& port_id) { |
| return callback_manager_->GetPendingCallbackCountForTest( // IN-TEST |
| script_context, port_id); |
| } |
| |
| std::unique_ptr<OneTimeMessageHandler::OneTimeMessageCallback> |
| OneTimeMessageHandler::CreateMessageResponseCallback(const PortId& port_id) { |
| return std::make_unique<OneTimeMessageHandler::OneTimeMessageCallback>( |
| base::BindOnce(&OneTimeMessageHandler::OnOneTimeMessageResponse, |
| weak_factory_.GetWeakPtr(), port_id)); |
| } |
| |
| std::unique_ptr<OneTimeMessageHandler::OneTimeMessageCallback> |
| OneTimeMessageHandler::CreatePromiseRejectedCallback(const PortId& port_id) { |
| return std::make_unique<OneTimeMessageHandler::OneTimeMessageCallback>( |
| base::BindOnce(&OneTimeMessageHandler::OnPromiseRejectedResponse, |
| weak_factory_.GetWeakPtr(), port_id)); |
| } |
| |
| std::unique_ptr<OneTimeMessageHandler::OneTimeMessageCallback> |
| OneTimeMessageHandler::CreateEventDispatchCallback( |
| const PortId& port_id, |
| std::optional<CallbackID> listener_error_callback_id) { |
| return std::make_unique<OneTimeMessageHandler::OneTimeMessageCallback>( |
| base::BindOnce(&OneTimeMessageHandler::OnEventFired, |
| weak_factory_.GetWeakPtr(), port_id, |
| listener_error_callback_id)); |
| } |
| |
| std::unique_ptr<OneTimeMessageHandler::OneTimeMessageCallback> |
| OneTimeMessageHandler::CreateListenerErrorCallback(const PortId& port_id) { |
| return std::make_unique<OneTimeMessageHandler::OneTimeMessageCallback>( |
| base::BindOnce(&OneTimeMessageHandler::OnListenerThrowsError, |
| weak_factory_.GetWeakPtr(), port_id)); |
| } |
| |
| void OneTimeMessageHandler::OnAllCallbacksCollected( |
| ScriptContext* script_context, |
| v8::Local<v8::Context> context, |
| const PortId& port_id) { |
| OneTimeMessageContextData* data = |
| GetPerContextData<OneTimeMessageContextData>(script_context->v8_context(), |
| kDontCreateIfMissing); |
| if (!data) { |
| return; |
| } |
| auto iter = data->receivers.find(port_id); |
| // The channel may already be closed (if the receiver replied before the reply |
| // callback was collected). |
| if (iter == data->receivers.end()) { |
| return; |
| } |
| // Since no more callbacks can be called the receiver doesn't need to be |
| // tracked anymore. |
| data->receivers.erase(port_id); |
| |
| // A different receiver may reply so don't close the channel. |
| CloseReceiverMessagePortOrChannel(script_context, port_id, |
| /*close_channel=*/false, |
| /*error=*/std::nullopt); |
| } |
| |
| bool OneTimeMessageHandler::DeliverMessageToReceiver( |
| ScriptContext* script_context, |
| const Message& message, |
| const PortId& target_port_id) { |
| DCHECK(!target_port_id.is_opener); |
| |
| v8::Isolate* isolate = script_context->isolate(); |
| v8::Local<v8::Context> context = script_context->v8_context(); |
| |
| bool handled = false; |
| |
| OneTimeMessageContextData* data = |
| GetPerContextData<OneTimeMessageContextData>(context, |
| kDontCreateIfMissing); |
| if (!data) |
| return handled; |
| |
| auto iter = data->receivers.find(target_port_id); |
| if (iter == data->receivers.end()) |
| return handled; |
| |
| handled = true; |
| OneTimeReceiver& port = iter->second; |
| |
| // This port is a receiver, so we invoke the onMessage event and provide a |
| // callback through which the port can respond. The port stays open until we |
| // receive a response. |
| auto message_response_callback = |
| CreateMessageResponseCallback(target_port_id); |
| // The v8 `reply` (a.k.a `sendResponse()`) function provided to |
| // `runtime.onMessage` listeners. |
| v8::Local<v8::Function> message_response_function = |
| callback_manager_->CreateRespondingFunction( |
| *script_context, target_port_id, |
| std::move(message_response_callback)); |
| |
| if (IsMessagePolyfillSupportEnabled()) { |
| port.message_response_function = |
| v8::Global<v8::Function>(isolate, message_response_function); |
| } |
| |
| v8::HandleScope handle_scope(isolate); |
| |
| // The current port is a receiver. The parsing should be fail-safe if this is |
| // a receiver for a native messaging host (i.e. the event name is |
| // kOnConnectNativeEvent). This is because a native messaging host can send |
| // malformed messages. |
| std::string error; |
| v8::Local<v8::Value> v8_message = messaging_util::MessageToV8( |
| context, message, |
| port.event_name == messaging_util::kOnConnectNativeEvent, &error); |
| |
| if (error.empty()) { |
| v8::Local<v8::Object> v8_sender = port.sender.Get(isolate); |
| v8::LocalVector<v8::Value> args( |
| isolate, {v8_message, v8_sender, message_response_function}); |
| |
| v8::Local<v8::Function> message_dispatched_function; |
| v8::Local<v8::Function> listener_throws_error_function; |
| // For runtime.onMessage, we require that the listener indicate if they |
| // intend to respond asynchronously. `message_dispatched_callback` will |
| // check the results of the listeners to determine if a listener indicated |
| // it intended to respond asynchronously. |
| if (port.event_name == messaging_util::kOnMessageEvent) { |
| CallbackID listener_throws_error_callback_id; |
| if (IsMessagePolyfillSupportEnabled()) { |
| auto listener_throws_error_callback = |
| CreateListenerErrorCallback(target_port_id); |
| listener_throws_error_callback_id = CallbackID::Create(); |
| listener_throws_error_function = |
| callback_manager_->CreateListenerThrowsErrorFunction( |
| *script_context, target_port_id, |
| std::move(listener_throws_error_callback), |
| listener_throws_error_callback_id); |
| } |
| auto message_dispatched_callback = CreateEventDispatchCallback( |
| target_port_id, listener_throws_error_callback_id); |
| message_dispatched_function = |
| callback_manager_->CreateEventDispatchFunction( |
| *script_context, target_port_id, |
| std::move(message_dispatched_callback)); |
| } |
| bindings_system_->api_system()->event_handler()->FireEventInContext( |
| port.event_name, context, &args, /*filter=*/nullptr, |
| message_dispatched_function, listener_throws_error_function); |
| } else { |
| console::AddMessage(script_context, |
| blink::mojom::ConsoleMessageLevel::kError, error); |
| } |
| |
| // Note: The context could be invalidated at this point! |
| |
| return handled; |
| } |
| |
| bool OneTimeMessageHandler::DeliverReplyToOpener(ScriptContext* script_context, |
| const Message& message, |
| const PortId& target_port_id) { |
| DCHECK(target_port_id.is_opener); |
| |
| v8::Local<v8::Context> v8_context = script_context->v8_context(); |
| bool handled = false; |
| |
| OneTimeMessageContextData* data = |
| GetPerContextData<OneTimeMessageContextData>(v8_context, |
| kDontCreateIfMissing); |
| if (!data) |
| return handled; |
| |
| auto iter = data->openers.find(target_port_id); |
| if (iter == data->openers.end()) |
| return handled; |
| |
| handled = true; |
| |
| // Note: make a copy of port, since we're about to free it. |
| const OneTimeOpener port = iter->second; |
| DCHECK_NE(-1, port.request_id); |
| |
| // We erase the opener now, since delivering the reply can cause JS to run, |
| // which could either invalidate the context or modify the |openers| |
| // collection (e.g., by sending another message). |
| data->openers.erase(iter); |
| |
| // This port was the opener, so the message is the response from the |
| // receiver. Invoke the callback and close the message port. |
| v8::Isolate* isolate = script_context->isolate(); |
| |
| // Parsing should be fail-safe for kNative channel type as native messaging |
| // hosts can send malformed messages. |
| std::string error; |
| v8::Local<v8::Value> v8_message = messaging_util::MessageToV8( |
| v8_context, message, port.channel_type == mojom::ChannelType::kNative, |
| &error); |
| |
| if (v8_message.IsEmpty()) { |
| // If the parsing fails, send back a v8::Undefined() message. |
| v8_message = v8::Undefined(isolate); |
| } |
| |
| v8::LocalVector<v8::Value> args(isolate, {v8_message}); |
| bindings_system_->api_system()->request_handler()->CompleteRequest( |
| port.request_id, args, error); |
| |
| bindings_system_->messaging_service()->CloseMessagePort( |
| script_context, target_port_id, /*close_channel=*/true); |
| |
| // Note: The context could be invalidated at this point! |
| |
| return handled; |
| } |
| |
| bool OneTimeMessageHandler::DisconnectReceiver(ScriptContext* script_context, |
| const PortId& port_id) { |
| v8::Local<v8::Context> context = script_context->v8_context(); |
| bool handled = false; |
| |
| OneTimeMessageContextData* data = |
| GetPerContextData<OneTimeMessageContextData>(context, |
| kDontCreateIfMissing); |
| if (!data) |
| return handled; |
| |
| auto iter = data->receivers.find(port_id); |
| if (iter == data->receivers.end()) |
| return handled; |
| |
| handled = true; |
| // With the channel closed, clean up the receiver port and its pending |
| // callbacks. This prevents further responses and avoids callback data leaks |
| // from indicated-but-never-sent asynchronous replies from the listener(s). |
| data->receivers.erase(iter); |
| callback_manager_->ClearCallbackDataForPortId(script_context, port_id); |
| |
| // The `ExtensionMessagePort` for this receiver's destructor handles message |
| // port (IPC) cleanup so we don't need to do that here. |
| return handled; |
| } |
| |
| bool OneTimeMessageHandler::DisconnectOpener(ScriptContext* script_context, |
| const PortId& port_id, |
| const std::string& error_message) { |
| bool handled = false; |
| |
| v8::Local<v8::Context> v8_context = script_context->v8_context(); |
| OneTimeMessageContextData* data = |
| GetPerContextData<OneTimeMessageContextData>(v8_context, |
| kDontCreateIfMissing); |
| if (!data) |
| return handled; |
| |
| auto iter = data->openers.find(port_id); |
| if (iter == data->openers.end()) |
| return handled; |
| |
| handled = true; |
| |
| // Note: make a copy of port, since we're about to free it. |
| const OneTimeOpener opener = iter->second; |
| DCHECK_NE(-1, opener.request_id); |
| |
| // We erase the opener now, since delivering the reply can cause JS to run, |
| // which could either invalidate the context or modify the |openers| |
| // collection (e.g., by sending another message). |
| data->openers.erase(iter); |
| |
| std::string error; |
| // Set the error for the message port. If the browser supplies an error, we |
| // always use that. Otherwise, the behavior is different for promise-based vs |
| // callback-based channels. |
| // For a promise-based channel, not receiving a response is fine (assuming the |
| // listener didn't indicate it would send one) - the extension may simply be |
| // waiting for confirmation that the message sent. |
| // In the callback-based scenario, we use the presence of the callback as an |
| // indication that the extension expected a specific response. This is an |
| // unfortunate behavior difference that we keep for backwards-compatibility in |
| // callback-based API calls. |
| if (!error_message.empty()) { |
| // If the browser supplied us with an error message, use that. |
| error = error_message; |
| } else if (opener.async_type == binding::AsyncResponseType::kCallback) { |
| error = "The message port closed before a response was received."; |
| } |
| |
| bindings_system_->api_system()->request_handler()->CompleteRequest( |
| opener.request_id, v8::LocalVector<v8::Value>(v8::Isolate::GetCurrent()), |
| error); |
| |
| // Note: The context could be invalidated at this point! |
| |
| return handled; |
| } |
| |
| void OneTimeMessageHandler::CloseReceiverMessagePortOrChannel( |
| ScriptContext* script_context, |
| const PortId& port_id, |
| bool close_channel, |
| std::optional<std::string> error) { |
| // With the message port closing callbacks aren't allowed to be called after |
| // this point so proactively clean them up. |
| callback_manager_->ClearCallbackDataForPortId(script_context, port_id); |
| |
| // If there was an error send it back to the message sender. |
| if (close_channel && error) { |
| messaging_service()->CloseMessagePort(script_context, port_id, |
| close_channel, *error); |
| return; |
| } |
| |
| // Otherwise if no error then just close the port and/or channel. |
| messaging_service()->CloseMessagePort(script_context, port_id, close_channel); |
| } |
| |
| void OneTimeMessageHandler::OnOneTimeMessageResponse( |
| const PortId& port_id, |
| gin::Arguments* arguments) { |
| v8::Isolate* isolate = arguments->isolate(); |
| v8::Local<v8::Context> context = isolate->GetCurrentContext(); |
| |
| // The listener may try replying after the context or the channel has been |
| // closed. Fail gracefully. |
| // TODO(devlin): At least in the case of the channel being closed (e.g. |
| // because the listener did not indicate it would reply asynchronously), it |
| // might be good to surface an error. |
| OneTimeMessageContextData* data = |
| GetPerContextData<OneTimeMessageContextData>(context, |
| kDontCreateIfMissing); |
| if (!data) |
| return; |
| |
| auto iter = data->receivers.find(port_id); |
| // The channel may already be closed (if a listener replied (promise rejected) |
| // or listener threw error). |
| if (iter == data->receivers.end()) |
| return; |
| // The response will be sent after this point so we no longer need to track |
| // the receiver. |
| data->receivers.erase(port_id); |
| |
| v8::Local<v8::Value> value; |
| // We allow omitting the message argument (e.g., sendMessage()). Default the |
| // value to undefined. |
| if (arguments->Length() > 0) |
| CHECK(arguments->GetNext(&value)); |
| else |
| value = v8::Undefined(isolate); |
| |
| ScriptContext* script_context = GetScriptContextFromV8Context(context); |
| |
| std::string message_creation_error; |
| std::unique_ptr<Message> message = messaging_util::MessageFromV8( |
| context, value, port_id.serialization_format, &message_creation_error); |
| if (!message) { |
| // Throw an error in the listener context. |
| arguments->ThrowTypeError(message_creation_error); |
| if (IsMessagePolyfillSupportEnabled()) { |
| // This is a "fatal" error for the channel so close it entirely. |
| CloseReceiverMessagePortOrChannel(script_context, port_id, |
| /*close_channel=*/true, |
| message_creation_error); |
| } |
| return; |
| } |
| |
| // If the MessagePortHost is still alive return the response. But the listener |
| // might be replying after the channel has been closed. |
| if (auto* message_port_host = messaging_service()->GetMessagePortHostIfExists( |
| script_context, port_id)) { |
| message_port_host->PostMessage(*message); |
| CloseReceiverMessagePortOrChannel(script_context, port_id, |
| /*close_channel=*/true, |
| /*error=*/std::nullopt); |
| } |
| |
| // With the message port closed no more callbacks should be called. |
| callback_manager_->ClearCallbackDataForPortId(script_context, port_id); |
| } |
| |
| v8::Local<v8::Function> OneTimeMessageHandler::OneTimeMessageCallbackManager:: |
| CreateDelayedOneTimeMessageCallback( |
| ScriptContext& script_context, |
| const PortId& port_id, |
| std::unique_ptr<OneTimeMessageCallback> callback, |
| bool cleanup_if_function_unused) { |
| return CreateDelayedOneTimeMessageCallback( |
| script_context, port_id, std::move(callback), cleanup_if_function_unused, |
| /*optional_callback_id=*/std::nullopt); |
| } |
| |
| v8::Local<v8::Function> OneTimeMessageHandler::OneTimeMessageCallbackManager:: |
| CreateDelayedOneTimeMessageCallback( |
| ScriptContext& script_context, |
| const PortId& port_id, |
| std::unique_ptr<OneTimeMessageCallback> callback, |
| bool cleanup_if_function_unused, |
| std::optional<CallbackID> optional_callback_id) { |
| CHECK(callback); |
| v8::Isolate* isolate = script_context.isolate(); |
| v8::Local<v8::Context> context = script_context.v8_context(); |
| |
| // We shouldn't need to check and get `data` like this if a listener has |
| // already responded, but it's much simpler to re-get it here than pass |
| // OneTimeMessageContextData into this method. |
| OneTimeMessageContextData* data = |
| GetPerContextData<OneTimeMessageContextData>( |
| context, CreatePerContextData::kDontCreateIfMissing); |
| // We will store `callback` in the per context data for later retrieval so it |
| // must exist for us to proceed. |
| CHECK(data); |
| |
| CallbackID callback_id; |
| if (optional_callback_id) { |
| callback_id = *optional_callback_id; |
| } else { |
| callback_id = OneTimeMessageHandler::CallbackID::Create(); |
| } |
| |
| // We convert to a v8::String here because we want to validate the string is |
| // still a valid `CallbackID` when we retrieve it from v8 when `function` is |
| // called. |
| v8::Local<v8::String> callback_id_v8_string = |
| gin::StringToV8(isolate, callback_id.ToString()); |
| v8::Local<v8::Function> function; |
| if (!v8::Function::New(context, &DelayedOneTimeMessageCallbackHelper, |
| callback_id_v8_string) |
| .ToLocal(&function)) { |
| NOTREACHED(); |
| } |
| |
| auto& port_callbacks = data->pending_receiver_callbacks[port_id]; |
| const auto& [callback_id_iter, callback_id_inserted] = |
| port_callbacks.try_emplace(callback_id, std::move(callback)); |
| // It could lead to unexpected behavior to add the same callback multiple |
| // times for the same one time message port. |
| CHECK(callback_id_inserted); |
| |
| if (cleanup_if_function_unused) { |
| new GCCallback( |
| &script_context, function, |
| /*callback=*/ |
| base::BindOnce(&OneTimeMessageHandler::OneTimeMessageCallbackManager:: |
| OnDelayedOneTimeMessageCallbackCollected, |
| weak_factory_.GetWeakPtr(), &script_context, port_id, |
| callback_id), |
| /*fallback=*/base::OnceClosure()); |
| } |
| |
| return function; |
| } |
| |
| void OneTimeMessageHandler::OneTimeMessageCallbackManager:: |
| OnDelayedOneTimeMessageCallbackCollected(ScriptContext* script_context, |
| const PortId& port_id, |
| CallbackID callback_id) { |
| // Note: we know |script_context| is still valid because the GC callback won't |
| // be called after context invalidation. |
| v8::HandleScope handle_scope(script_context->isolate()); |
| OneTimeMessageContextData* data = |
| GetPerContextData<OneTimeMessageContextData>(script_context->v8_context(), |
| kDontCreateIfMissing); |
| // ScriptContext invalidation and PerContextData cleanup happen "around" the |
| // same time, but there aren't strict guarantees about ordering. It's possible |
| // the data was collected. |
| if (!data) |
| return; |
| |
| // Since there is no way to call the callback anymore, we can remove it from |
| // the pending callbacks and delete the port entry if this was the last |
| // callback. Note: this should occur before returning early due to the |
| // receiver being deleted because multiple pending callbacks can be created |
| // for each message or `DisconnectReceiver()` could be called before we get |
| // here. |
| if (auto port_id_iter = data->pending_receiver_callbacks.find(port_id); |
| port_id_iter != data->pending_receiver_callbacks.end()) { |
| auto& callbacks = port_id_iter->second; |
| callbacks.erase(callback_id); |
| if (!callbacks.empty()) { |
| // If we've deleted the callback, but there's still a remaining callback |
| // then this should only happen iff polyfill support is enabled. |
| DCHECK(IsMessagePolyfillSupportEnabled()); |
| // When polyfill support is enabled we'll create two callbacks (message |
| // response and promise reject) that can be collected at different times. |
| // Only the last callback of these two collected should continue on to |
| // close the port. Otherwise it could cause the other callback to not |
| // fully run if called because it'll think the port was already closed. |
| return; |
| } |
| // There are no more callbacks remaining, so delete the unused `PortId` key. |
| data->pending_receiver_callbacks.erase(port_id_iter); |
| } |
| |
| // Notify `message_handler_` so it can update the port state. |
| message_handler_->OnAllCallbacksCollected( |
| script_context, script_context->v8_context(), port_id); |
| // More callbacks could be collected later so we'll leave the callback data |
| // alone after closing the port. |
| } |
| |
| std::optional<std::string> OneTimeMessageHandler::GetErrorMessageFromValue( |
| v8::Isolate* isolate, |
| v8::Local<v8::Value> possible_error_value) { |
| if (!possible_error_value->IsNativeError()) { |
| return std::nullopt; |
| } |
| |
| v8::Local<v8::Message> error_message = |
| v8::Exception::CreateMessage(isolate, possible_error_value); |
| std::string error_message_from_v8; |
| bool error_message_string_convert_success = |
| gin::Converter<std::string>::FromV8(isolate, |
| error_message->Get().As<v8::Value>(), |
| &error_message_from_v8); |
| |
| if (!error_message_string_convert_success || error_message_from_v8.empty()) { |
| return std::nullopt; |
| } |
| |
| return error_message_from_v8; |
| } |
| |
| void OneTimeMessageHandler::OnPromiseRejectedResponse( |
| const PortId& port_id, |
| gin::Arguments* arguments) { |
| CHECK(IsMessagePolyfillSupportEnabled()); |
| |
| CHECK(arguments); |
| v8::Isolate* isolate = arguments->isolate(); |
| v8::Local<v8::Context> context = isolate->GetCurrentContext(); |
| |
| // The promise may reject after the context or the channel has been closed. |
| // Fail gracefully. |
| OneTimeMessageContextData* data = |
| GetPerContextData<OneTimeMessageContextData>(context, |
| kDontCreateIfMissing); |
| if (!data) { |
| return; |
| } |
| auto iter = data->receivers.find(port_id); |
| // The channel may already be closed (if a listener already replied, or |
| // listener threw error). |
| if (iter == data->receivers.end()) { |
| return; |
| } |
| // The promise reject will be sent as an error response after this point so we |
| // no longer need to track the receiver. |
| data->receivers.erase(port_id); |
| |
| v8::Local<v8::Value> promise_reject_value; |
| // This is safe to CHECK() because when a promise rejects it always provides a |
| // value. Even if `reject()` (with no argument) is called we see `undefined` |
| // for `promise_reject_value`. |
| CHECK(arguments->Length() > 0); |
| CHECK(arguments->GetNext(&promise_reject_value)); |
| |
| // If promise rejection reason is a JS Error type then close the message port |
| // with the Error's .message property. Otherwise return a generic error |
| // message. |
| // TODO(crbug.com/439644930): Support sending the listener's stack trace along |
| // with the rejection error. mozilla/webextension-polyfill doesn't support it |
| // currently, but plans to (see |
| // https://github.com/mozilla/webextension-polyfill/issues/210). |
| std::optional<std::string> error_message_from_value = |
| GetErrorMessageFromValue(isolate, promise_reject_value); |
| |
| std::string error_message = |
| error_message_from_value |
| ? *error_message_from_value |
| : "A runtime.onMessage listener's promise rejected without an Error"; |
| |
| // TODO(crbug.com/439644930): Support sending the listener's stack trace along |
| // with the rejection error. mozilla/webextension-polyfill doesn't support it |
| // currently, but plans to (see |
| // https://github.com/mozilla/webextension-polyfill/issues/210). |
| |
| ScriptContext* script_context = GetScriptContextFromV8Context(context); |
| CloseReceiverMessagePortOrChannel(script_context, port_id, |
| /*close_channel=*/true, error_message); |
| } |
| |
| void OneTimeMessageHandler::OnListenerThrowsError(const PortId& port_id, |
| gin::Arguments* arguments) { |
| CHECK(IsMessagePolyfillSupportEnabled()); |
| |
| v8::Isolate* isolate = arguments->isolate(); |
| v8::Local<v8::Context> context = isolate->GetCurrentContext(); |
| |
| OneTimeMessageContextData* data = |
| GetPerContextData<OneTimeMessageContextData>(context, |
| kDontCreateIfMissing); |
| |
| // Dispatching can invalidate the context so if it is then we won't be able to |
| // inform the message sender. |
| if (!data) { |
| return; |
| } |
| |
| auto iter = data->receivers.find(port_id); |
| // The channel may already be closed (if a listener already replied). |
| if (iter == data->receivers.end()) { |
| return; |
| } |
| // The listener thrown error will be sent as an error response after this |
| // point so we no longer need to track the receiver. |
| data->receivers.erase(port_id); |
| |
| v8::Local<v8::Value> listener_thrown_value; |
| CHECK(arguments->Length() > 0); |
| CHECK(arguments->GetNext(&listener_thrown_value)); |
| |
| std::optional<std::string> error_message_from_value = |
| GetErrorMessageFromValue(isolate, listener_thrown_value); |
| std::string error_message = |
| error_message_from_value |
| ? *error_message_from_value |
| : "Error message from listener couldn't be parsed or was empty."; |
| |
| // TODO(crbug.com/439644930): Support sending the listener's stack trace along |
| // with the rejection error. mozilla/webextension-polyfill doesn't support it |
| // currently, but plans to (see |
| // https://github.com/mozilla/webextension-polyfill/issues/210). |
| |
| ScriptContext* script_context = GetScriptContextFromV8Context(context); |
| CloseReceiverMessagePortOrChannel(script_context, port_id, |
| /*close_channel=*/true, error_message); |
| } |
| |
| bool OneTimeMessageHandler::CheckAndHandleAsyncListenerReply( |
| v8::Isolate* isolate, |
| v8::Local<v8::Context> context, |
| ScriptContext& script_context, |
| v8::Local<v8::Value> result, |
| const PortId& port_id, |
| // TODO(crbug.com/40753031): Move the creation of |
| // `promise_resolved_function` to just before promise handler attachment. It |
| // doesn't need to be created before that point. |
| v8::Local<v8::Function> promise_resolved_function) { |
| v8::Local<v8::Array> results_array = |
| GetListenerResultArray(isolate, context, result, "results"); |
| if (results_array.IsEmpty()) { |
| return false; |
| } |
| |
| bool will_reply_async = false; |
| for (uint32_t i = 0; i < results_array->Length(); ++i) { |
| v8::MaybeLocal<v8::Value> maybe_result = results_array->Get(context, i); |
| v8::Local<v8::Value> listener_return; |
| // Assume the result could throw due to changes at runtime by the |
| // extension's JS code. |
| if (!maybe_result.ToLocal(&listener_return)) { |
| continue; |
| } |
| |
| // Check if any of the results is indicating it will reply async by |
| // returning `true`. |
| if (listener_return->IsBoolean() && |
| listener_return.As<v8::Boolean>()->Value()) { |
| will_reply_async = true; |
| } |
| |
| // If promise returns are not supported, then we don't need to attach any |
| // callbacks and can return early once we find at least one listener that |
| // wants to reply asynchronously |
| if (!IsMessagePolyfillSupportEnabled() && will_reply_async) { |
| return true; |
| } |
| |
| // Check if any of the returns are a promise, indicating the listener will |
| // reply async. Attach callbacks for both the promise resolving or |
| // rejecting. This is so that whatever the promise settles to is considered |
| // the listener replying to the message sender with the settled value. |
| if (IsMessagePolyfillSupportEnabled() && listener_return->IsPromise()) { |
| auto promise_rejected_response_callback = |
| CreatePromiseRejectedCallback(port_id); |
| v8::Local<v8::Function> promise_rejected_function = |
| callback_manager_->CreateRespondingFunction( |
| script_context, port_id, |
| std::move(promise_rejected_response_callback)); |
| std::ignore = listener_return.As<v8::Promise>()->Then( |
| context, promise_resolved_function, promise_rejected_function); |
| // TODO(crbug.com/40753031): Consider setting lastError for caller when |
| // promise is rejected. |
| will_reply_async = true; |
| } |
| } |
| |
| return will_reply_async; |
| } |
| |
| void OneTimeMessageHandler::OnEventFired( |
| const PortId& port_id, |
| std::optional<CallbackID> listener_error_callback_id, |
| gin::Arguments* arguments) { |
| v8::Isolate* isolate = arguments->isolate(); |
| v8::Local<v8::Context> context = isolate->GetCurrentContext(); |
| |
| v8::Local<v8::Value> result; |
| if (arguments->Length() > 0) { |
| CHECK(arguments->GetNext(&result)); |
| } else { |
| result = v8::Undefined(isolate); |
| } |
| |
| // The context could be tearing down by the time the event is fully |
| // dispatched. |
| OneTimeMessageContextData* data = |
| GetPerContextData<OneTimeMessageContextData>(context, |
| kDontCreateIfMissing); |
| if (!data) |
| return; |
| |
| ScriptContext* script_context = GetScriptContextFromV8Context(context); |
| DCHECK(script_context) |
| << "script context was destroyed before runtime.onMessage listener " |
| "results could be processed."; |
| |
| // Cleanup listener error callback if created since it shouldn't be possible |
| // for synchronous thrown errors to appear after all listeners have finished |
| // being dispatched to. |
| if (IsMessagePolyfillSupportEnabled() && listener_error_callback_id) { |
| callback_manager_->DeleteCallbackDataForCallbackId( |
| script_context, port_id, *listener_error_callback_id); |
| } |
| |
| auto iter = data->receivers.find(port_id); |
| // The channel may be closed (if the listener replied or threw an error). |
| if (iter == data->receivers.end()) { |
| return; |
| } |
| |
| OneTimeReceiver& port = iter->second; |
| |
| v8::Local<v8::Function> promise_resolved_function; |
| if (IsMessagePolyfillSupportEnabled()) { |
| promise_resolved_function = port.message_response_function.Get(isolate); |
| // Ensure the global function doesn't outlive port closing. |
| port.message_response_function.SetWeak(); |
| } |
| |
| if (CheckAndHandleAsyncListenerReply(isolate, context, *script_context, |
| result, port_id, |
| promise_resolved_function)) { |
| // Inform the browser that one of the listeners said they would be replying |
| // later and leave the channel open. |
| if (auto* message_port_host = |
| messaging_service()->GetMessagePortHostIfExists(script_context, |
| port_id)) { |
| message_port_host->ResponsePending(); |
| } |
| return; |
| } |
| |
| // The listener did not reply and did not indicate it would reply later from |
| // any of its listeners. Close the message port. Don't close the channel |
| // because another listener (in a separate context) may reply. |
| data->receivers.erase(port_id); |
| CloseReceiverMessagePortOrChannel(script_context, port_id, |
| /*close_channel=*/false, |
| /*error=*/std::nullopt); |
| } |
| |
| // This must be defined in the .cc due to `OneTimeMessageHandler`'s header being |
| // included in `NativeExtensionBindingsSystem` causing a circular dependency. |
| NativeRendererMessagingService* OneTimeMessageHandler::messaging_service() { |
| return bindings_system_->messaging_service(); |
| } |
| |
| } // namespace extensions |