| // Copyright 2019 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/js_injection/browser/js_to_browser_messaging.h" |
| |
| #include "base/containers/contains.h" |
| #include "base/memory/raw_ptr.h" |
| #include "components/back_forward_cache/back_forward_cache_disable.h" |
| #include "components/js_injection/browser/web_message.h" |
| #include "components/js_injection/browser/web_message_host.h" |
| #include "components/js_injection/browser/web_message_host_factory.h" |
| #include "components/js_injection/browser/web_message_reply_proxy.h" |
| #include "components/js_injection/common/interfaces.mojom-forward.h" |
| #include "components/origin_matcher/origin_matcher.h" |
| #include "content/public/browser/disallow_activation_reason.h" |
| #include "content/public/browser/document_user_data.h" |
| #include "content/public/browser/render_process_host.h" |
| #include "content/public/browser/weak_document_ptr.h" |
| #include "content/public/browser/web_contents.h" |
| #include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h" |
| #include "third_party/blink/public/common/messaging/message_port_descriptor.h" |
| #include "third_party/blink/public/common/messaging/string_message_codec.h" |
| #include "url/origin.h" |
| #include "url/url_util.h" |
| |
| namespace js_injection { |
| namespace { |
| |
| // We want to pass a string "null" for local file schemes, to make it |
| // consistent to the Blink side SecurityOrigin serialization. When both |
| // setAllow{File,Universal}AccessFromFileURLs are false, Blink::SecurityOrigin |
| // will be serialized as string "null" for local file schemes, but when |
| // setAllowFileAccessFromFileURLs is true, Blink::SecurityOrigin will be |
| // serialized as the scheme, which will be inconsistentt to this place. In |
| // this case we want to let developer to know that local files are not safe, |
| // so we still pass "null". |
| std::string GetOriginString(const url::Origin& source_origin) { |
| return base::Contains(url::GetLocalSchemes(), source_origin.scheme()) |
| ? "null" |
| : source_origin.Serialize(); |
| } |
| |
| // Used for queueing messages posted during prerendering. DocumentUserData |
| // should be appropriate for managing them as the messages should be discarded |
| // when an associated document is gone. |
| class JsToBrowserMessagingDocumentUserData |
| : public content::DocumentUserData<JsToBrowserMessagingDocumentUserData> { |
| public: |
| ~JsToBrowserMessagingDocumentUserData() override = default; |
| |
| std::vector<std::unique_ptr<WebMessage>>& queued_messages() { |
| return queued_messages_; |
| } |
| |
| private: |
| friend class DocumentUserData<JsToBrowserMessagingDocumentUserData>; |
| |
| explicit JsToBrowserMessagingDocumentUserData( |
| content::RenderFrameHost* render_frame_host) |
| : content::DocumentUserData<JsToBrowserMessagingDocumentUserData>( |
| render_frame_host) {} |
| |
| std::vector<std::unique_ptr<WebMessage>> queued_messages_; |
| |
| DOCUMENT_USER_DATA_KEY_DECL(); |
| }; |
| |
| DOCUMENT_USER_DATA_KEY_IMPL(JsToBrowserMessagingDocumentUserData); |
| |
| } // namespace |
| |
| class JsToBrowserMessaging::ReplyProxyImpl : public WebMessageReplyProxy { |
| public: |
| ReplyProxyImpl( |
| content::RenderFrameHost* render_frame_host, |
| mojo::PendingAssociatedRemote<mojom::BrowserToJsMessaging> |
| java_to_js_messaging, |
| mojo::SharedAssociatedRemote<mojom::BrowserToJsMessagingFactory> factory) |
| : document_(render_frame_host->GetWeakDocumentPtr()), |
| java_to_js_messaging_(std::move(java_to_js_messaging)), |
| factory_(std::move(factory)) {} |
| ReplyProxyImpl(const ReplyProxyImpl&) = delete; |
| ReplyProxyImpl& operator=(const ReplyProxyImpl&) = delete; |
| ~ReplyProxyImpl() override = default; |
| |
| // WebMessageReplyProxy: |
| void PostWebMessage(blink::WebMessagePayload message) override { |
| if (document_.AsRenderFrameHostIfValid() && |
| document_.AsRenderFrameHostIfValid()->GetLifecycleState() == |
| content::RenderFrameHost::LifecycleState::kInBackForwardCache) { |
| // If the document associated with the reply proxy is in BFCache, evict |
| // the page from BFCache. This is because the page can't process the |
| // message while frozen due to BFCaching, and if we queue the message, |
| // the callers might not expect the message to be delayed for a long |
| // time (or even not sent at all). |
| content::BackForwardCache::DisableForRenderFrameHost( |
| document_.AsRenderFrameHostIfValid(), |
| back_forward_cache::DisabledReason( |
| back_forward_cache::DisabledReasonId:: |
| kPostMessageByWebViewClient)); |
| return; |
| } |
| EnsureBrowserToJsMessaging(); |
| java_to_js_messaging_->OnPostMessage(std::move(message)); |
| } |
| |
| void EnsureBrowserToJsMessaging() { |
| if (!java_to_js_messaging_ && factory_) { |
| factory_->SendBrowserToJsMessaging( |
| java_to_js_messaging_.BindNewEndpointAndPassReceiver()); |
| } |
| } |
| |
| content::Page& GetPage() override { |
| // We can't make up the Page if the document is already destructed. Callers |
| // of this function must ensure to not call this if we're in that state. |
| CHECK(document_.AsRenderFrameHostIfValid()); |
| return document_.AsRenderFrameHostIfValid()->GetPage(); |
| } |
| |
| private: |
| content::WeakDocumentPtr document_; |
| mojo::AssociatedRemote<mojom::BrowserToJsMessaging> java_to_js_messaging_; |
| mojo::SharedAssociatedRemote<mojom::BrowserToJsMessagingFactory> factory_; |
| }; |
| |
| JsToBrowserMessaging::JsToBrowserMessaging( |
| content::RenderFrameHost* render_frame_host, |
| mojo::PendingAssociatedReceiver<mojom::JsToBrowserMessaging> receiver, |
| mojo::PendingAssociatedRemote<mojom::BrowserToJsMessagingFactory> |
| browser_to_js_factory, |
| WebMessageHostFactory* factory, |
| const origin_matcher::OriginMatcher& origin_matcher) |
| : render_frame_host_(render_frame_host), |
| connection_factory_(factory), |
| origin_matcher_(origin_matcher), |
| browser_to_js_factory_(std::move(browser_to_js_factory)) { |
| receiver_.Bind(std::move(receiver)); |
| } |
| |
| JsToBrowserMessaging::~JsToBrowserMessaging() = default; |
| |
| void JsToBrowserMessaging::OnRenderFrameHostActivated() { |
| JsToBrowserMessagingDocumentUserData* data = |
| JsToBrowserMessagingDocumentUserData::GetForCurrentDocument( |
| render_frame_host_); |
| if (!data) { |
| return; |
| } |
| |
| if (!host_) { |
| return; |
| } |
| |
| for (auto& message : data->queued_messages()) { |
| host_->OnPostMessage(std::move(message)); |
| } |
| data->queued_messages().clear(); |
| } |
| |
| void JsToBrowserMessaging::PostMessage( |
| blink::WebMessagePayload message, |
| std::vector<blink::MessagePortDescriptor> ports) { |
| DCHECK(render_frame_host_); |
| |
| // For prerendering, messages will be queued until activation. |
| if (!render_frame_host_->IsInLifecycleState( |
| content::RenderFrameHost::LifecycleState::kPrerendering) && |
| render_frame_host_->IsInactiveAndDisallowActivation( |
| content::DisallowActivationReasonId::kJsInjectionPostMessage)) { |
| return; |
| } |
| |
| content::WebContents* web_contents = |
| content::WebContents::FromRenderFrameHost(render_frame_host_); |
| |
| if (!web_contents) |
| return; |
| |
| const url::Origin top_level_origin = |
| render_frame_host_->GetMainFrame()->GetLastCommittedOrigin(); |
| // |source_origin| has no race with this PostMessage call, because of |
| // associated mojo channel, the committed origin message and PostMessage are |
| // in sequence. |
| const url::Origin source_origin = |
| render_frame_host_->GetLastCommittedOrigin(); |
| |
| if (!origin_matcher_.Matches(source_origin)) |
| return; |
| |
| // SetBrowserToJsMessaging must be called before this. |
| DCHECK(reply_proxy_); |
| reply_proxy_->EnsureBrowserToJsMessaging(); |
| |
| if (!host_) { |
| const std::string top_level_origin_string = |
| GetOriginString(top_level_origin); |
| const std::string origin_string = GetOriginString(source_origin); |
| |
| // Check if this is the main frame of the primary or prerendered page. |
| const bool is_main_frame = !render_frame_host_->GetParentOrOuterDocument(); |
| |
| host_ = |
| connection_factory_->CreateHost(top_level_origin_string, origin_string, |
| is_main_frame, reply_proxy_.get()); |
| #if DCHECK_IS_ON() |
| top_level_origin_string_ = top_level_origin_string; |
| origin_string_ = origin_string; |
| is_main_frame_ = is_main_frame; |
| #endif |
| if (!host_) |
| return; |
| } |
| // The origin and whether this is the main frame should not change once |
| // PostMessage() has been received. |
| #if DCHECK_IS_ON() |
| DCHECK_EQ(GetOriginString(top_level_origin), top_level_origin_string_); |
| DCHECK_EQ(GetOriginString(source_origin), origin_string_); |
| DCHECK_EQ(is_main_frame_, !render_frame_host_->GetParentOrOuterDocument()); |
| #endif |
| std::unique_ptr<WebMessage> web_message = std::make_unique<WebMessage>(); |
| web_message->message = std::move(message); |
| web_message->ports = std::move(ports); |
| |
| if (render_frame_host_->IsInLifecycleState( |
| content::RenderFrameHost::LifecycleState::kPrerendering)) { |
| // Queue `WebMessage`s received while prerendering. They are flushed on |
| // `OnRenderFrameHostActivated`. |
| JsToBrowserMessagingDocumentUserData* data = |
| JsToBrowserMessagingDocumentUserData::GetOrCreateForCurrentDocument( |
| render_frame_host_); |
| data->queued_messages().push_back(std::move(web_message)); |
| return; |
| } |
| |
| host_->OnPostMessage(std::move(web_message)); |
| } |
| |
| void JsToBrowserMessaging::SetBrowserToJsMessaging( |
| mojo::PendingAssociatedRemote<mojom::BrowserToJsMessaging> |
| java_to_js_messaging) { |
| // TODO(crbug.com/40752101): this should really call |
| // IsInactiveAndDisallowReactivation(). |
| |
| // A RenderFrame may inject JsToBrowserMessaging in the JavaScript context |
| // more than once because of reusing of RenderFrame. |
| host_.reset(); |
| reply_proxy_ = std::make_unique<ReplyProxyImpl>( |
| render_frame_host_, std::move(java_to_js_messaging), |
| browser_to_js_factory_); |
| } |
| |
| } // namespace js_injection |