diff options
Diffstat (limited to 'chromium/components/js_injection/renderer')
6 files changed, 571 insertions, 0 deletions
diff --git a/chromium/components/js_injection/renderer/BUILD.gn b/chromium/components/js_injection/renderer/BUILD.gn new file mode 100644 index 00000000000..3cdd5294923 --- /dev/null +++ b/chromium/components/js_injection/renderer/BUILD.gn @@ -0,0 +1,25 @@ +# Copyright 2020 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. + +source_set("renderer") { + sources = [ + "js_binding.cc", + "js_binding.h", + "js_communication.cc", + "js_communication.h", + ] + + deps = [ + "//base", + "//components/js_injection/common", + "//components/js_injection/common:common_mojom", + "//content/public/common", + "//content/public/renderer", + "//gin", + "//mojo/public/cpp/bindings", + "//third_party/blink/public:blink", + "//url", + "//v8", + ] +} diff --git a/chromium/components/js_injection/renderer/DEPS b/chromium/components/js_injection/renderer/DEPS new file mode 100644 index 00000000000..33ec23100fe --- /dev/null +++ b/chromium/components/js_injection/renderer/DEPS @@ -0,0 +1,4 @@ +include_rules = [ + "+gin", + "+v8/include/v8.h", +] diff --git a/chromium/components/js_injection/renderer/js_binding.cc b/chromium/components/js_injection/renderer/js_binding.cc new file mode 100644 index 00000000000..f4c5798990c --- /dev/null +++ b/chromium/components/js_injection/renderer/js_binding.cc @@ -0,0 +1,246 @@ +// Copyright 2019 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 "components/js_injection/renderer/js_binding.h" + +#include <vector> + +#include "base/strings/string_util.h" +#include "components/js_injection/renderer/js_communication.h" +#include "content/public/renderer/render_frame.h" +#include "gin/data_object_builder.h" +#include "gin/handle.h" +#include "gin/object_template_builder.h" +#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h" +#include "third_party/blink/public/common/messaging/message_port_channel.h" +#include "third_party/blink/public/platform/web_security_origin.h" +#include "third_party/blink/public/web/blink.h" +#include "third_party/blink/public/web/web_frame.h" +#include "third_party/blink/public/web/web_local_frame.h" +#include "third_party/blink/public/web/web_message_port_converter.h" +#include "v8/include/v8.h" + +namespace { +constexpr char kPostMessage[] = "postMessage"; +constexpr char kOnMessage[] = "onmessage"; +constexpr char kAddEventListener[] = "addEventListener"; +constexpr char kRemoveEventListener[] = "removeEventListener"; +} // anonymous namespace + +namespace js_injection { + +gin::WrapperInfo JsBinding::kWrapperInfo = {gin::kEmbedderNativeGin}; + +// static +std::unique_ptr<JsBinding> JsBinding::Install( + content::RenderFrame* render_frame, + const base::string16& js_object_name, + JsCommunication* js_java_configurator) { + CHECK(!js_object_name.empty()) + << "JavaScript wrapper name shouldn't be empty"; + + v8::Isolate* isolate = blink::MainThreadIsolate(); + v8::HandleScope handle_scope(isolate); + v8::Local<v8::Context> context = + render_frame->GetWebFrame()->MainWorldScriptContext(); + if (context.IsEmpty()) + return nullptr; + + v8::Context::Scope context_scope(context); + std::unique_ptr<JsBinding> js_binding( + new JsBinding(render_frame, js_object_name, js_java_configurator)); + gin::Handle<JsBinding> bindings = + gin::CreateHandle(isolate, js_binding.get()); + if (bindings.IsEmpty()) + return nullptr; + + v8::Local<v8::Object> global = context->Global(); + global + ->CreateDataProperty(context, + gin::StringToSymbol(isolate, js_object_name), + bindings.ToV8()) + .Check(); + + return js_binding; +} + +JsBinding::JsBinding(content::RenderFrame* render_frame, + const base::string16& js_object_name, + JsCommunication* js_java_configurator) + : render_frame_(render_frame), + js_object_name_(js_object_name), + js_java_configurator_(js_java_configurator) { + mojom::JsToBrowserMessaging* js_to_java_messaging = + js_java_configurator_->GetJsToJavaMessage(js_object_name_); + if (js_to_java_messaging) { + js_to_java_messaging->SetBrowserToJsMessaging( + receiver_.BindNewEndpointAndPassRemote()); + } +} + +JsBinding::~JsBinding() = default; + +void JsBinding::OnPostMessage(const base::string16& message) { + v8::Isolate* isolate = blink::MainThreadIsolate(); + v8::HandleScope handle_scope(isolate); + + blink::WebLocalFrame* web_frame = render_frame_->GetWebFrame(); + if (!web_frame) + return; + + v8::Local<v8::Context> context = web_frame->MainWorldScriptContext(); + if (context.IsEmpty()) + return; + + v8::Context::Scope context_scope(context); + // Setting verbose makes the exception get reported to the default + // uncaught-exception handlers, rather than just being silently swallowed. + v8::TryCatch try_catch(isolate); + try_catch.SetVerbose(true); + + // Simulate MessageEvent's data property. See + // https://html.spec.whatwg.org/multipage/comms.html#messageevent + v8::Local<v8::Object> event = + gin::DataObjectBuilder(isolate).Set("data", message).Build(); + v8::Local<v8::Value> argv[] = {event}; + + v8::Local<v8::Object> self = GetWrapper(isolate).ToLocalChecked(); + v8::Local<v8::Function> on_message = GetOnMessage(isolate); + if (!on_message.IsEmpty()) { + web_frame->RequestExecuteV8Function(context, on_message, self, 1, argv, + nullptr); + } + + for (const auto& listener : listeners_) { + web_frame->RequestExecuteV8Function(context, listener.Get(isolate), self, 1, + argv, nullptr); + } +} + +void JsBinding::ReleaseV8GlobalObjects() { + listeners_.clear(); + on_message_.Reset(); +} + +gin::ObjectTemplateBuilder JsBinding::GetObjectTemplateBuilder( + v8::Isolate* isolate) { + return gin::Wrappable<JsBinding>::GetObjectTemplateBuilder(isolate) + .SetMethod(kPostMessage, &JsBinding::PostMessage) + .SetMethod(kAddEventListener, &JsBinding::AddEventListener) + .SetMethod(kRemoveEventListener, &JsBinding::RemoveEventListener) + .SetProperty(kOnMessage, &JsBinding::GetOnMessage, + &JsBinding::SetOnMessage); +} + +void JsBinding::PostMessage(gin::Arguments* args) { + base::string16 message; + if (!args->GetNext(&message)) { + args->ThrowError(); + return; + } + + std::vector<blink::MessagePortChannel> ports; + std::vector<v8::Local<v8::Object>> objs; + // If we get more than two arguments and the second argument is not an array + // of ports, we can't process. + if (args->Length() >= 2 && !args->GetNext(&objs)) { + args->ThrowError(); + return; + } + + for (auto& obj : objs) { + base::Optional<blink::MessagePortChannel> port = + blink::WebMessagePortConverter::DisentangleAndExtractMessagePortChannel( + args->isolate(), obj); + // If the port is null we should throw an exception. + if (!port.has_value()) { + args->ThrowError(); + return; + } + ports.emplace_back(port.value()); + } + + mojom::JsToBrowserMessaging* js_to_java_messaging = + js_java_configurator_->GetJsToJavaMessage(js_object_name_); + if (js_to_java_messaging) { + js_to_java_messaging->PostMessage( + message, blink::MessagePortChannel::ReleaseHandles(ports)); + } +} + +// AddEventListener() needs to match EventTarget's AddEventListener() in blink. +// It takes |type|, |listener| parameters, we ignore the |options| parameter. +// See https://dom.spec.whatwg.org/#dom-eventtarget-addeventlistener +void JsBinding::AddEventListener(gin::Arguments* args) { + std::string type; + if (!args->GetNext(&type)) { + args->ThrowError(); + return; + } + + // We only support message event. + if (type != "message") + return; + + v8::Local<v8::Function> listener; + if (!args->GetNext(&listener)) + return; + + // Should be at most 3 parameters. + if (args->Length() > 3) { + args->ThrowError(); + return; + } + + if (base::Contains(listeners_, listener)) + return; + + v8::Local<v8::Context> context = args->GetHolderCreationContext(); + listeners_.push_back( + v8::Global<v8::Function>(context->GetIsolate(), listener)); +} + +// RemoveEventListener() needs to match EventTarget's RemoveEventListener() in +// blink. It takes |type|, |listener| parameters, we ignore |options| parameter. +// See https://dom.spec.whatwg.org/#dom-eventtarget-removeeventlistener +void JsBinding::RemoveEventListener(gin::Arguments* args) { + std::string type; + if (!args->GetNext(&type)) { + args->ThrowError(); + return; + } + + // We only support message event. + if (type != "message") + return; + + v8::Local<v8::Function> listener; + if (!args->GetNext(&listener)) + return; + + // Should be at most 3 parameters. + if (args->Length() > 3) { + args->ThrowError(); + return; + } + + auto iter = std::find(listeners_.begin(), listeners_.end(), listener); + if (iter == listeners_.end()) + return; + + listeners_.erase(iter); +} + +v8::Local<v8::Function> JsBinding::GetOnMessage(v8::Isolate* isolate) { + return on_message_.Get(isolate); +} + +void JsBinding::SetOnMessage(v8::Isolate* isolate, v8::Local<v8::Value> value) { + if (value->IsFunction()) + on_message_.Reset(isolate, value.As<v8::Function>()); + else + on_message_.Reset(); +} + +} // namespace js_injection diff --git a/chromium/components/js_injection/renderer/js_binding.h b/chromium/components/js_injection/renderer/js_binding.h new file mode 100644 index 00000000000..1fca33f9110 --- /dev/null +++ b/chromium/components/js_injection/renderer/js_binding.h @@ -0,0 +1,87 @@ +// Copyright 2019 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. + +#ifndef COMPONENTS_JS_INJECTION_RENDERER_JS_BINDING_H_ +#define COMPONENTS_JS_INJECTION_RENDERER_JS_BINDING_H_ + +#include <string> + +#include "base/auto_reset.h" +#include "base/strings/string16.h" +#include "components/js_injection/common/interfaces.mojom.h" +#include "gin/arguments.h" +#include "gin/wrappable.h" +#include "mojo/public/cpp/bindings/associated_receiver.h" +#include "mojo/public/cpp/bindings/associated_remote.h" + +namespace v8 { +template <typename T> +class Global; +class Function; +} // namespace v8 + +namespace content { +class RenderFrame; +} // namespace content + +namespace js_injection { +class JsCommunication; +// A gin::Wrappable class used for providing JavaScript API. We will inject the +// object of this class to JavaScript world in JsCommunication. +// JsCommunication will own at most one instance of this class. When the +// RenderFrame gone or another DidClearWindowObject comes, the instance will be +// destroyed. +class JsBinding : public gin::Wrappable<JsBinding>, + public mojom::BrowserToJsMessaging { + public: + static gin::WrapperInfo kWrapperInfo; + + static std::unique_ptr<JsBinding> Install( + content::RenderFrame* render_frame, + const base::string16& js_object_name, + JsCommunication* js_java_configurator); + + // mojom::BrowserToJsMessaging implementation. + void OnPostMessage(const base::string16& message) override; + + void ReleaseV8GlobalObjects(); + + ~JsBinding() final; + + private: + explicit JsBinding(content::RenderFrame* render_frame, + const base::string16& js_object_name, + JsCommunication* js_java_configurator); + + // gin::Wrappable implementation + gin::ObjectTemplateBuilder GetObjectTemplateBuilder( + v8::Isolate* isolate) final; + + // For jsObject.postMessage(message[, ports]) JavaScript API. + void PostMessage(gin::Arguments* args); + // For jsObject.addEventListener("message", listener) JavaScript API. + void AddEventListener(gin::Arguments* args); + // For jsObject.removeEventListener("message", listener) JavaScript API. + void RemoveEventListener(gin::Arguments* args); + // For get jsObject.onmessage. + v8::Local<v8::Function> GetOnMessage(v8::Isolate* isolate); + // For set jsObject.onmessage. + void SetOnMessage(v8::Isolate* isolate, v8::Local<v8::Value> value); + + content::RenderFrame* render_frame_; + base::string16 js_object_name_; + v8::Global<v8::Function> on_message_; + std::vector<v8::Global<v8::Function>> listeners_; + // |js_java_configurator| owns JsBinding objects, so it will out live + // JsBinding's life cycle, it is safe to access it. + JsCommunication* js_java_configurator_; + + mojo::AssociatedReceiver<mojom::BrowserToJsMessaging> receiver_{this}; + + DISALLOW_COPY_AND_ASSIGN(JsBinding); +}; + +} // namespace js_injection + +#endif // COMPONENTS_JS_INJECTION_RENDERER_JS_BINDING_H_ diff --git a/chromium/components/js_injection/renderer/js_communication.cc b/chromium/components/js_injection/renderer/js_communication.cc new file mode 100644 index 00000000000..dd648297f20 --- /dev/null +++ b/chromium/components/js_injection/renderer/js_communication.cc @@ -0,0 +1,133 @@ +// Copyright 2019 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 "components/js_injection/renderer/js_communication.h" + +#include "components/js_injection/common/origin_matcher.h" +#include "components/js_injection/renderer/js_binding.h" +#include "content/public/common/isolated_world_ids.h" +#include "content/public/renderer/render_frame.h" +#include "third_party/blink/public/common/associated_interfaces/associated_interface_registry.h" +#include "third_party/blink/public/platform/task_type.h" +#include "third_party/blink/public/web/web_local_frame.h" +#include "third_party/blink/public/web/web_script_source.h" +#include "url/gurl.h" +#include "url/origin.h" + +namespace js_injection { + +struct JsCommunication::JsObjectInfo { + OriginMatcher origin_matcher; + mojo::AssociatedRemote<mojom::JsToBrowserMessaging> js_to_java_messaging; +}; + +struct JsCommunication::DocumentStartJavaScript { + OriginMatcher origin_matcher; + blink::WebString script; + int32_t script_id; +}; + +JsCommunication::JsCommunication(content::RenderFrame* render_frame) + : RenderFrameObserver(render_frame), + RenderFrameObserverTracker<JsCommunication>(render_frame) { + render_frame->GetAssociatedInterfaceRegistry()->AddInterface( + base::BindRepeating(&JsCommunication::BindPendingReceiver, + base::Unretained(this))); +} + +JsCommunication::~JsCommunication() = default; + +void JsCommunication::SetJsObjects( + std::vector<mojom::JsObjectPtr> js_object_ptrs) { + JsObjectMap js_objects; + for (const auto& js_object : js_object_ptrs) { + const auto& js_object_info_pair = js_objects.insert( + {js_object->js_object_name, std::make_unique<JsObjectInfo>()}); + JsObjectInfo* js_object_info = js_object_info_pair.first->second.get(); + js_object_info->origin_matcher = js_object->origin_matcher; + js_object_info->js_to_java_messaging = + mojo::AssociatedRemote<mojom::JsToBrowserMessaging>( + std::move(js_object->js_to_browser_messaging)); + } + js_objects_.swap(js_objects); +} + +void JsCommunication::AddDocumentStartScript( + mojom::DocumentStartJavaScriptPtr script_ptr) { + DocumentStartJavaScript* script = new DocumentStartJavaScript{ + script_ptr->origin_matcher, + blink::WebString::FromUTF16(script_ptr->script), script_ptr->script_id}; + scripts_.push_back(std::unique_ptr<DocumentStartJavaScript>(script)); +} + +void JsCommunication::RemoveDocumentStartScript(int32_t script_id) { + for (auto it = scripts_.begin(); it != scripts_.end(); ++it) { + if ((*it)->script_id == script_id) { + scripts_.erase(it); + break; + } + } +} + +void JsCommunication::DidClearWindowObject() { + if (inside_did_clear_window_object_) + return; + + base::AutoReset<bool> flag_entry(&inside_did_clear_window_object_, true); + + url::Origin frame_origin = + url::Origin(render_frame()->GetWebFrame()->GetSecurityOrigin()); + std::vector<std::unique_ptr<JsBinding>> js_bindings; + js_bindings.reserve(js_objects_.size()); + for (const auto& js_object : js_objects_) { + if (!js_object.second->origin_matcher.Matches(frame_origin)) + continue; + js_bindings.push_back( + JsBinding::Install(render_frame(), js_object.first, this)); + } + js_bindings_.swap(js_bindings); +} + +void JsCommunication::WillReleaseScriptContext(v8::Local<v8::Context> context, + int32_t world_id) { + // We created v8 global objects only in the main world, should clear them only + // when this is for main world. + if (world_id != content::ISOLATED_WORLD_ID_GLOBAL) + return; + + for (const auto& js_binding : js_bindings_) + js_binding->ReleaseV8GlobalObjects(); +} + +void JsCommunication::OnDestruct() { + delete this; +} + +void JsCommunication::RunScriptsAtDocumentStart() { + url::Origin frame_origin = + url::Origin(render_frame()->GetWebFrame()->GetSecurityOrigin()); + for (const auto& script : scripts_) { + if (!script->origin_matcher.Matches(frame_origin)) + continue; + render_frame()->GetWebFrame()->ExecuteScript( + blink::WebScriptSource(script->script)); + } +} + +void JsCommunication::BindPendingReceiver( + mojo::PendingAssociatedReceiver<mojom::JsCommunication> pending_receiver) { + receiver_.Bind(std::move(pending_receiver), + render_frame()->GetTaskRunner( + blink::TaskType::kInternalNavigationAssociated)); +} + +mojom::JsToBrowserMessaging* JsCommunication::GetJsToJavaMessage( + const base::string16& js_object_name) { + auto iterator = js_objects_.find(js_object_name); + if (iterator == js_objects_.end()) + return nullptr; + return iterator->second->js_to_java_messaging.get(); +} + +} // namespace js_injection diff --git a/chromium/components/js_injection/renderer/js_communication.h b/chromium/components/js_injection/renderer/js_communication.h new file mode 100644 index 00000000000..8986e784c65 --- /dev/null +++ b/chromium/components/js_injection/renderer/js_communication.h @@ -0,0 +1,76 @@ +// Copyright 2019 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. + +#ifndef COMPONENTS_JS_INJECTION_RENDERER_JS_COMMUNICATION_H_ +#define COMPONENTS_JS_INJECTION_RENDERER_JS_COMMUNICATION_H_ + +#include <vector> + +#include "base/strings/string16.h" +#include "components/js_injection/common/interfaces.mojom.h" +#include "content/public/renderer/render_frame_observer.h" +#include "content/public/renderer/render_frame_observer_tracker.h" +#include "mojo/public/cpp/bindings/associated_receiver.h" +#include "mojo/public/cpp/bindings/associated_remote.h" +#include "third_party/blink/public/platform/web_string.h" + +namespace content { +class RenderFrame; +} + +namespace js_injection { + +class JsBinding; + +class JsCommunication + : public mojom::JsCommunication, + public content::RenderFrameObserver, + public content::RenderFrameObserverTracker<JsCommunication> { + public: + explicit JsCommunication(content::RenderFrame* render_frame); + ~JsCommunication() override; + + // mojom::JsCommunication implementation + void SetJsObjects(std::vector<mojom::JsObjectPtr> js_object_ptrs) override; + void AddDocumentStartScript( + mojom::DocumentStartJavaScriptPtr script_ptr) override; + void RemoveDocumentStartScript(int32_t script_id) override; + + // RenderFrameObserver implementation + void DidClearWindowObject() override; + void WillReleaseScriptContext(v8::Local<v8::Context> context, + int32_t world_id) override; + void OnDestruct() override; + + void RunScriptsAtDocumentStart(); + + mojom::JsToBrowserMessaging* GetJsToJavaMessage( + const base::string16& js_object_name); + + private: + struct JsObjectInfo; + struct DocumentStartJavaScript; + + void BindPendingReceiver( + mojo::PendingAssociatedReceiver<mojom::JsCommunication> pending_receiver); + + using JsObjectMap = std::map<base::string16, std::unique_ptr<JsObjectInfo>>; + JsObjectMap js_objects_; + + // In some cases DidClearWindowObject will be called twice in a row, we need + // to prevent doing multiple injection in that case. + bool inside_did_clear_window_object_ = false; + + std::vector<std::unique_ptr<DocumentStartJavaScript>> scripts_; + std::vector<std::unique_ptr<JsBinding>> js_bindings_; + + // Associated with legacy IPC channel. + mojo::AssociatedReceiver<mojom::JsCommunication> receiver_{this}; + + DISALLOW_COPY_AND_ASSIGN(JsCommunication); +}; + +} // namespace js_injection + +#endif // COMPONENTS_JS_INJECTION_RENDERER_JS_COMMUNICATION_H_ |