blob: a77f2e978dd58bc977af17c0d94181e81d1be00c [file] [log] [blame]
// Copyright 2012 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/browser/api/messaging/extension_message_port.h"
#include <memory>
#include <optional>
#include <set>
#include <utility>
#include "base/containers/contains.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/notreached.h"
#include "base/scoped_observation.h"
#include "base/strings/strcat.h"
#include "base/types/optional_util.h"
#include "base/types/pass_key.h"
#include "components/back_forward_cache/back_forward_cache_disable.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/common/content_features.h"
#include "extensions/browser/api/messaging/channel_endpoint.h"
#include "extensions/browser/extension_host.h"
#include "extensions/browser/extension_web_contents_observer.h"
#include "extensions/browser/process_manager.h"
#include "extensions/browser/process_manager_observer.h"
#include "extensions/browser/service_worker/service_worker_host.h"
#include "extensions/common/api/messaging/message.h"
#include "extensions/common/api/messaging/messaging_endpoint.h"
#include "extensions/common/manifest_handlers/background_info.h"
#include "extensions/common/mojom/message_port.mojom-shared.h"
#include "services/metrics/public/cpp/ukm_recorder.h"
#include "services/metrics/public/cpp/ukm_source_id.h"
namespace extensions {
namespace {
std::string PortIdToString(const extensions::PortId& port_id) {
return base::StrCat({port_id.GetChannelId().first.ToString(), ":",
base::NumberToString(port_id.GetChannelId().second)});
}
using PassKey = base::PassKey<ExtensionMessagePort>;
const char kReceivingEndDoesntExistError[] =
// TODO(lazyboy): Test these in service worker implementation.
"Could not establish connection. Receiving end does not exist.";
const char kClosedWhileResponsePendingError[] =
"A listener indicated an asynchronous response by returning true, but the "
"message channel closed before a response was received";
const char kClosedWhenPageEntersBFCache[] =
"The page keeping the extension port is moved into back/forward cache, so "
"the message channel is closed.";
} // namespace
// Helper class to detect when frames are destroyed.
class ExtensionMessagePort::FrameTracker : public content::WebContentsObserver,
public ProcessManagerObserver {
public:
explicit FrameTracker(ExtensionMessagePort* port) : port_(port) {}
FrameTracker(const FrameTracker&) = delete;
FrameTracker& operator=(const FrameTracker&) = delete;
~FrameTracker() override = default;
void TrackExtensionProcessFrames() {
pm_observation_.Observe(ProcessManager::Get(port_->browser_context_));
}
void TrackTabFrames(content::WebContents* tab) { Observe(tab); }
private:
// content::WebContentsObserver overrides:
void RenderFrameDeleted(
content::RenderFrameHost* render_frame_host) override {
port_->UnregisterFrame(render_frame_host);
}
void DidFinishNavigation(
content::NavigationHandle* navigation_handle) override {
if (base::FeatureList::IsEnabled(
features::kDisconnectExtensionMessagePortWhenPageEntersBFCache) &&
navigation_handle->HasCommitted()) {
// Close the channel and force all the ports from the channel to be
// closed when the old RFH is going to be stored in BFCache. They will
// not be able to receive any message sent through the port.
content::RenderFrameHost* previous_rfh = content::RenderFrameHost::FromID(
navigation_handle->GetPreviousRenderFrameHostId());
if (previous_rfh &&
previous_rfh->GetLifecycleState() ==
content::RenderFrameHost::LifecycleState::kInBackForwardCache) {
if (port_->UnregisterFramesUnderMainFrame(
previous_rfh, kClosedWhenPageEntersBFCache)) {
// Since the channel and the port is already closed, we don't have to
// run the following block to unregister the frames any more.
return;
}
}
}
// There are a number of possible scenarios for the navigation:
// 1. Same-document navigation - Don't unregister the frame, since it can
// still use the port.
// 2. Cross-document navigation, reusing the RenderFrameHost - Unregister
// the frame, since the new document is not allowed to use the port.
// 3. Cross-document navigation, with a new RenderFrameHost - Since the
// navigated-to document has a new RFH, the port can not be registered for
// it, so it doesn't matter whether we unregister it or not. If the
// navigated-from document is stored in the back-forward cache, don't
// unregister the frame (see note below). If it is not cached, the frame
// will be unregistered when the RFH is deleted.
// 4. Restoring a cached frame from back-forward cache or activating a
// prerendered frame - This is similar to (3) in that the navigation changes
// RFH, with the difference that the RFH is not new and so may be
// registered. Don't unregister the frame in this case since it may still
// use the port.
// Note that we don't just disconnect channels when a frame is bf-cached
// since when such a document is later restored, there is no "load" and so a
// message channel won't be immediately available to extensions.
// Contrast this with a normal load where an extension is able to inject
// scripts at "document_start" and set up message ports.
if (navigation_handle->HasCommitted() &&
!navigation_handle->IsSameDocument() &&
!navigation_handle->IsPageActivation()) {
// Note: This unregisters the _new_ RenderFrameHost. In case a new RFH was
// created for this navigation, this will be a no-op, since we haven't
// seen it before. In case the RFH is reused for the navigation, this will
// correctly unregister the frame, to avoid messages intended for the
// previous document being sent to the new document. If the navigated-to
// RFH is served from cache, keep the port alive.
port_->UnregisterFrame(navigation_handle->GetRenderFrameHost());
}
}
// extensions::ProcessManagerObserver overrides:
void OnExtensionFrameUnregistered(
const std::string& extension_id,
content::RenderFrameHost* render_frame_host) override {
if (extension_id == port_->extension_id_)
port_->UnregisterFrame(render_frame_host);
}
void OnServiceWorkerUnregistered(const WorkerId& worker_id) override {
port_->UnregisterWorker(worker_id);
}
base::ScopedObservation<ProcessManager, ProcessManagerObserver>
pm_observation_{this};
raw_ptr<ExtensionMessagePort> port_; // Owns this FrameTracker.
};
ExtensionMessagePort::ExtensionMessagePort(
base::WeakPtr<ChannelDelegate> channel_delegate,
const PortId& port_id,
const std::string& extension_id,
content::RenderFrameHost* render_frame_host,
bool include_child_frames)
: MessagePort(std::move(channel_delegate), port_id),
extension_id_(extension_id),
browser_context_(render_frame_host->GetProcess()->GetBrowserContext()),
frame_tracker_(new FrameTracker(this)) {
content::WebContents* tab =
content::WebContents::FromRenderFrameHost(render_frame_host);
CHECK(tab);
frame_tracker_->TrackTabFrames(tab);
if (include_child_frames) {
// TODO(https://crbug.com/1227787) We don't yet support MParch for
// prerender so make sure `include_child_frames` is only provided for
// primary main frames.
CHECK(render_frame_host->IsInPrimaryMainFrame());
render_frame_host->ForEachRenderFrameHostWithAction(
[tab, this](content::RenderFrameHost* render_frame_host) {
// RegisterFrame should only be called for frames associated with
// `tab` and not any inner WebContents.
if (content::WebContents::FromRenderFrameHost(render_frame_host) !=
tab) {
return content::RenderFrameHost::FrameIterationAction::
kSkipChildren;
}
RegisterFrame(render_frame_host);
return content::RenderFrameHost::FrameIterationAction::kContinue;
});
} else {
RegisterFrame(render_frame_host);
}
}
// static
std::unique_ptr<ExtensionMessagePort> ExtensionMessagePort::CreateForExtension(
base::WeakPtr<ChannelDelegate> channel_delegate,
const PortId& port_id,
const ExtensionId& extension_id,
content::BrowserContext* browser_context) {
auto port = std::make_unique<ExtensionMessagePort>(
channel_delegate, port_id, extension_id, browser_context, PassKey());
port->frame_tracker_ = std::make_unique<FrameTracker>(port.get());
port->frame_tracker_->TrackExtensionProcessFrames();
port->for_all_extension_contexts_ = true;
auto* process_manager = ProcessManager::Get(browser_context);
auto all_hosts =
process_manager->GetRenderFrameHostsForExtension(extension_id);
for (content::RenderFrameHost* render_frame_host : all_hosts) {
port->RegisterFrame(render_frame_host);
}
std::vector<WorkerId> running_workers =
process_manager->GetServiceWorkersForExtension(extension_id);
for (const WorkerId& running_worker_id : running_workers)
port->RegisterWorker(running_worker_id);
return port;
}
// static
std::unique_ptr<ExtensionMessagePort> ExtensionMessagePort::CreateForEndpoint(
base::WeakPtr<ChannelDelegate> channel_delegate,
const PortId& port_id,
const std::string& extension_id,
const ChannelEndpoint& endpoint,
mojo::PendingAssociatedRemote<extensions::mojom::MessagePort> message_port,
mojo::PendingAssociatedReceiver<extensions::mojom::MessagePortHost>
message_port_host) {
auto port = std::make_unique<ExtensionMessagePort>(
channel_delegate, port_id, extension_id, endpoint.browser_context(),
PassKey());
port->frame_tracker_ = std::make_unique<FrameTracker>(port.get());
if (endpoint.is_for_render_frame()) {
content::RenderFrameHost* render_frame_host = endpoint.GetRenderFrameHost();
content::WebContents* tab =
content::WebContents::FromRenderFrameHost(render_frame_host);
CHECK(tab);
port->frame_tracker_->TrackTabFrames(tab);
auto& receiver = port->frames_[render_frame_host->GetGlobalFrameToken()];
receiver.Bind(std::move(message_port));
receiver.set_disconnect_handler(base::BindOnce(
&ExtensionMessagePort::Prune, base::Unretained(port.get())));
} else {
port->frame_tracker_->TrackExtensionProcessFrames();
auto& receiver = port->service_workers_[endpoint.GetWorkerId()];
receiver.Bind(std::move(message_port));
receiver.set_disconnect_handler(base::BindOnce(
&ExtensionMessagePort::Prune, base::Unretained(port.get())));
}
port->AddReceiver(std::move(message_port_host), endpoint.render_process_id(),
endpoint.port_context());
return port;
}
ExtensionMessagePort::ExtensionMessagePort(
base::WeakPtr<ChannelDelegate> channel_delegate,
const PortId& port_id,
const ExtensionId& extension_id,
content::BrowserContext* browser_context,
PassKey)
: MessagePort(std::move(channel_delegate), port_id),
extension_id_(extension_id),
browser_context_(browser_context) {}
ExtensionMessagePort::~ExtensionMessagePort() = default;
void ExtensionMessagePort::Prune() {
std::vector<content::GlobalRenderFrameHostToken> frames_to_unregister;
for (auto& frame : frames_) {
if (!frame.second.is_connected()) {
frames_to_unregister.push_back(frame.first);
}
}
for (const auto& frame_token : frames_to_unregister) {
if (UnregisterFrame(frame_token)) {
return;
}
}
std::vector<WorkerId> workers_to_unregister;
for (auto& worker : service_workers_) {
if (!worker.second.is_connected()) {
workers_to_unregister.push_back(worker.first);
}
}
for (const auto& worker : workers_to_unregister) {
if (UnregisterWorker(worker)) {
return;
}
}
}
void ExtensionMessagePort::RemoveCommonFrames(const MessagePort& port) {
// This should be called before OnConnect is called.
CHECK(frames_.empty());
// Avoid overlap in the set of frames to make sure that it does not matter
// when UnregisterFrame is called.
std::erase_if(
pending_frames_,
[&port](const content::GlobalRenderFrameHostToken& frame_token) {
return port.HasFrame(frame_token);
});
}
bool ExtensionMessagePort::HasFrame(
const content::GlobalRenderFrameHostToken& frame_token) const {
return base::Contains(frames_, frame_token) ||
base::Contains(pending_frames_, frame_token);
}
bool ExtensionMessagePort::IsValidPort() {
return !frames_.empty() || !service_workers_.empty() ||
!pending_frames_.empty() || !pending_service_workers_.empty();
}
void ExtensionMessagePort::RevalidatePort() {
// Checks whether the frames to which this port is tied at its construction
// are still aware of this port's existence. Frames that don't know about
// the port are removed from the set of frames. This should be used for opener
// ports because the frame may be navigated before the port was initialized.
// Only opener ports need to be revalidated, because these are created in the
// renderer before the browser knows about them.
DCHECK(!for_all_extension_contexts_);
DCHECK_EQ(frames_.size() + service_workers_.size() + pending_frames_.size() +
pending_service_workers_.size(),
1U)
<< "RevalidatePort() should only be called for opener ports which "
"correspond to a single 'context'.";
// NOTE: There is only one opener target.
if (!frames_.empty()) {
if (!frames_.begin()->second.is_connected()) {
UnregisterFrame(frames_.begin()->first);
}
return;
}
if (!service_workers_.empty()) {
if (!service_workers_.begin()->second.is_connected()) {
UnregisterWorker(service_workers_.begin()->first);
}
return;
}
}
void ExtensionMessagePort::DispatchOnConnect(
mojom::ChannelType channel_type,
const std::string& channel_name,
std::optional<base::Value::Dict> source_tab,
const ExtensionApiFrameIdMap::FrameData& source_frame,
int guest_process_id,
int guest_render_frame_routing_id,
const MessagingEndpoint& source_endpoint,
const std::string& target_extension_id,
const GURL& source_url,
std::optional<url::Origin> source_origin) {
mojom::TabConnectionInfoPtr source = mojom::TabConnectionInfo::New();
// Source document ID should exist if and only if there is a source tab.
DCHECK_EQ(!!source_tab, !!source_frame.document_id);
if (source_tab) {
source->tab = source_tab->Clone();
source->document_id = source_frame.document_id.ToString();
source->document_lifecycle = ToString(source_frame.document_lifecycle);
}
source->frame_id = source_frame.frame_id;
mojom::ExternalConnectionInfoPtr info = mojom::ExternalConnectionInfo::New();
info->target_id = target_extension_id;
info->source_endpoint = source_endpoint;
info->source_url = source_url;
info->source_origin = std::move(source_origin);
info->guest_process_id = guest_process_id;
info->guest_render_frame_routing_id = guest_render_frame_routing_id;
// `ShouldSkipFrameForBFCache` could mutate `pending_frames_` so we
// take it before iterating on it.
auto pending_frames = std::move(pending_frames_);
for (const auto& frame_token : pending_frames) {
auto* frame = content::RenderFrameHost::FromFrameToken(frame_token);
if (!frame || ShouldSkipFrameForBFCache(frame)) {
continue;
}
mojo::PendingAssociatedReceiver<mojom::MessagePort> message_port;
mojo::PendingAssociatedRemote<mojom::MessagePortHost> message_port_host;
auto& receiver = frames_[frame_token];
receiver.Bind(message_port.InitWithNewEndpointAndPassRemote());
receiver.set_disconnect_handler(
base::BindOnce(&ExtensionMessagePort::Prune, base::Unretained(this)));
AddReceiver(message_port_host.InitWithNewEndpointAndPassReceiver(),
frame->GetProcess()->GetID(),
PortContext::ForFrame(frame->GetRoutingID()));
ExtensionWebContentsObserver::GetForWebContents(
content::WebContents::FromRenderFrameHost(frame))
->GetLocalFrameChecked(frame)
.DispatchOnConnect(
port_id_, channel_type, channel_name, source.Clone(), info.Clone(),
std::move(message_port), std::move(message_port_host),
base::BindOnce(&ExtensionMessagePort::OnConnectResponse,
weak_ptr_factory_.GetWeakPtr()));
}
for (const auto& worker : pending_service_workers_) {
auto* host = ServiceWorkerHost::GetWorkerFor(worker);
if (host) {
auto* service_worker_remote = host->GetServiceWorker();
if (!service_worker_remote) {
continue;
}
mojo::PendingAssociatedReceiver<mojom::MessagePort> message_port;
mojo::PendingAssociatedRemote<mojom::MessagePortHost> message_port_host;
auto& receiver = service_workers_[worker];
receiver.Bind(message_port.InitWithNewEndpointAndPassRemote());
receiver.set_disconnect_handler(
base::BindOnce(&ExtensionMessagePort::Prune, base::Unretained(this)));
AddReceiver(message_port_host.InitWithNewEndpointAndPassReceiver(),
worker.render_process_id,
PortContext::ForWorker(worker.thread_id, worker.version_id,
worker.extension_id));
service_worker_remote->DispatchOnConnect(
port_id_, channel_type, channel_name, source.Clone(), info.Clone(),
std::move(message_port), std::move(message_port_host),
base::BindOnce(&ExtensionMessagePort::OnConnectResponse,
weak_ptr_factory_.GetWeakPtr()));
}
}
pending_frames_.clear();
pending_service_workers_.clear();
}
void ExtensionMessagePort::OnConnectResponse(bool success) {
// For the unsuccessful case the port will be cleaned up in `Prune` when
// the mojo channels are disconnected.
if (success) {
port_was_created_ = true;
}
}
void ExtensionMessagePort::DispatchOnDisconnect(
const std::string& error_message) {
SendToPort(base::BindRepeating(
[](const std::string& error_message, mojom::MessagePort* port) {
port->DispatchDisconnect(error_message);
},
std::ref(error_message)));
}
void ExtensionMessagePort::DispatchOnMessage(const Message& message) {
// We increment activity for every message that passes through the channel.
// This is important for long-lived ports, which only keep an extension
// alive so long as they are being actively used.
IncrementLazyKeepaliveCount(Activity::MESSAGE);
// Since we are now receiving a message, we can mark any asynchronous reply
// that may have been pending for this port as no longer pending.
asynchronous_reply_pending_ = false;
SendToPort(base::BindRepeating(
[](const Message& message, mojom::MessagePort* port) {
port->DeliverMessage(message);
},
std::ref(message)));
DecrementLazyKeepaliveCount(Activity::MESSAGE);
}
void ExtensionMessagePort::IncrementLazyKeepaliveCount(
Activity::Type activity_type) {
ProcessManager* pm = ProcessManager::Get(browser_context_);
ExtensionHost* host = pm->GetBackgroundHostForExtension(extension_id_);
if (host && BackgroundInfo::HasLazyBackgroundPage(host->extension())) {
pm->IncrementLazyKeepaliveCount(host->extension(), activity_type,
PortIdToString(port_id_));
}
// Keep track of the background host, so when we decrement, we only do so if
// the host hasn't reloaded.
background_host_ptr_ = host;
if (!IsServiceWorkerActivity(activity_type)) {
return;
}
// Increment keepalive count for service workers of the extension managed by
// this port.
// TODO(https://crbug.com/1514471): Add a check to only increment count if
// the port is in lazy context.
for (const auto& worker_id :
pm->GetServiceWorkersForExtension(extension_id_)) {
base::Uuid request_uuid = pm->IncrementServiceWorkerKeepaliveCount(
worker_id,
should_have_strong_keepalive()
? content::ServiceWorkerExternalRequestTimeoutType::kDoesNotTimeout
: content::ServiceWorkerExternalRequestTimeoutType::kDefault,
activity_type, PortIdToString(port_id_));
pending_keepalive_uuids_[worker_id].push_back(std::move(request_uuid));
}
}
void ExtensionMessagePort::DecrementLazyKeepaliveCount(
Activity::Type activity_type) {
ProcessManager* pm = ProcessManager::Get(browser_context_);
ExtensionHost* host = pm->GetBackgroundHostForExtension(extension_id_);
if (host && host == background_host_ptr_) {
pm->DecrementLazyKeepaliveCount(host->extension(), activity_type,
PortIdToString(port_id_));
return;
}
if (!IsServiceWorkerActivity(activity_type)) {
return;
}
// Decrement keepalive count for service workers of the extension managed by
// this port.
// TODO(https://crbug.com/1514471): Add a check to only decrement count if
// the port is in lazy context.
for (const auto& worker_id :
pm->GetServiceWorkersForExtension(extension_id_)) {
auto iter = pending_keepalive_uuids_.find(worker_id);
if (iter == pending_keepalive_uuids_.end()) {
// We may not have a pending keepalive if this worker wasn't created at
// the time the message channel opened.
continue;
}
base::Uuid request_uuid = std::move(iter->second.back());
iter->second.pop_back();
if (iter->second.empty()) {
pending_keepalive_uuids_.erase(iter);
}
pm->DecrementServiceWorkerKeepaliveCount(
worker_id, request_uuid, activity_type, PortIdToString(port_id_));
}
}
void ExtensionMessagePort::NotifyResponsePending() {
asynchronous_reply_pending_ = true;
}
void ExtensionMessagePort::OpenPort(int process_id,
const PortContext& port_context) {
DCHECK((port_context.is_for_render_frame() &&
port_context.frame->routing_id != MSG_ROUTING_NONE) ||
(port_context.is_for_service_worker() &&
port_context.worker->thread_id != kMainThreadId) ||
for_all_extension_contexts_);
port_was_created_ = true;
}
void ExtensionMessagePort::ClosePort(int process_id,
int routing_id,
int worker_thread_id) {
const bool is_for_service_worker = worker_thread_id != kMainThreadId;
DCHECK(is_for_service_worker || routing_id != MSG_ROUTING_NONE);
if (is_for_service_worker) {
UnregisterWorker(process_id, worker_thread_id);
} else if (auto* render_frame_host =
content::RenderFrameHost::FromID(process_id, routing_id)) {
UnregisterFrame(render_frame_host);
}
}
void ExtensionMessagePort::CloseChannel(
std::optional<std::string> error_message) {
std::string error;
if (error_message.has_value()) {
error = std::move(error_message).value();
} else if (!port_was_created_) {
error = kReceivingEndDoesntExistError;
} else if (asynchronous_reply_pending_) {
error = kClosedWhileResponsePendingError;
}
if (weak_channel_delegate_) {
weak_channel_delegate_->CloseChannel(port_id_, error);
}
}
void ExtensionMessagePort::RegisterFrame(
content::RenderFrameHost* render_frame_host) {
// Only register a RenderFrameHost whose RenderFrame has been created, to
// ensure that we are notified of frame destruction. Without this check,
// `pending_frames_` can contain a stale token because RenderFrameDeleted
// is not triggered for `render_frame_host`.
if (render_frame_host->IsRenderFrameLive()) {
pending_frames_.insert(render_frame_host->GetGlobalFrameToken());
}
}
bool ExtensionMessagePort::UnregisterFrame(
content::RenderFrameHost* render_frame_host) {
return UnregisterFrame(render_frame_host->GetGlobalFrameToken());
}
bool ExtensionMessagePort::UnregisterFrame(
const content::GlobalRenderFrameHostToken& frame_token) {
frames_.erase(frame_token);
pending_frames_.erase(frame_token);
if (!IsValidPort()) {
CloseChannel();
return true;
}
return false;
}
bool ExtensionMessagePort::UnregisterFramesUnderMainFrame(
content::RenderFrameHost* main_frame,
std::optional<std::string> error_message) {
CHECK(pending_frames_.empty());
if (std::erase_if(frames_,
[&main_frame](const auto& item) {
auto* frame =
content::RenderFrameHost::FromFrameToken(item.first);
return !frame ||
frame->GetOutermostMainFrame() == main_frame;
}) != 0 &&
!IsValidPort()) {
CloseChannel(error_message);
return true;
}
return false;
}
void ExtensionMessagePort::RegisterWorker(const WorkerId& worker_id) {
DCHECK(!worker_id.extension_id.empty());
pending_service_workers_.insert(worker_id);
}
bool ExtensionMessagePort::UnregisterWorker(const WorkerId& worker_id) {
if (extension_id_ != worker_id.extension_id)
return false;
service_workers_.erase(worker_id);
pending_service_workers_.erase(worker_id);
if (!IsValidPort()) {
CloseChannel();
return true;
}
return false;
}
void ExtensionMessagePort::UnregisterWorker(int render_process_id,
int worker_thread_id) {
DCHECK_NE(kMainThreadId, worker_thread_id);
// Note: We iterate through *each* workers belonging to this port to find the
// worker we are interested in. Since there will only be a handful of such
// workers, this is OK.
for (auto iter = service_workers_.begin(); iter != service_workers_.end();) {
if (iter->first.render_process_id == render_process_id &&
iter->first.thread_id == worker_thread_id) {
service_workers_.erase(iter);
break;
} else {
++iter;
}
}
if (!IsValidPort()) {
CloseChannel();
}
}
void ExtensionMessagePort::SendToPort(SendCallback send_callback) {
// We should have called OnConnect before SentToPort.
CHECK(pending_frames_.empty());
CHECK(pending_service_workers_.empty());
std::vector<content::GlobalRenderFrameHostToken> frame_targets;
// Build the list of targets.
for (const auto& item : frames_) {
frame_targets.push_back(item.first);
}
for (const auto& target : frame_targets) {
auto item = frames_.find(target);
// `ShouldSkipFrameForBFCache` can mutate `frames_`, so verify the frame
// still exists.
if (item == frames_.end()) {
continue;
}
auto* frame = content::RenderFrameHost::FromFrameToken(item->first);
if (frame && !ShouldSkipFrameForBFCache(frame)) {
send_callback.Run(item->second.get());
}
}
for (const auto& running_worker : service_workers_) {
send_callback.Run(running_worker.second.get());
}
}
bool ExtensionMessagePort::IsServiceWorkerActivity(
Activity::Type activity_type) {
switch (activity_type) {
case Activity::MESSAGE:
return true;
case Activity::MESSAGE_PORT:
// long-lived message channels (such as through runtime.connect()) only
// increment keepalive when a message is sent so that a port doesn't count
// as a single, long-running task.
return is_for_onetime_channel() || should_have_strong_keepalive();
default:
// Extension message port should not check for other activity types.
NOTREACHED();
return false;
}
}
bool ExtensionMessagePort::ShouldSkipFrameForBFCache(
content::RenderFrameHost* render_frame_host) {
// Frames in the BackForwardCache are not allowed to receive messages (or
// even have them queued). In such a case, we evict the page from the cache
// and "drop" the message (See comment in `DidFinishNavigation()`).
// Note: Since this will cause the frame to be deleted, we do this here
// instead of in the loop above to avoid modifying `frames_` while it is
// being iterated.
//
// This could cause the same page to be evicted multiple times if it has
// multiple frames receiving this message. This is harmless as the reason is
// the same in every case. Also multiple extensions may send messages before
// the page is actually evicted. The last one will be the one the user
// sees. It is not worth the effort to present all of them to the user. It's
// unlikely they will see the same one every time and if they do, when they
// fix that one, they will see the others.
//
// TODO (crbug.com/1382623): currently we only make use of the base URL,
// it's also possible to get the full URL from extension ID so it could
// provide more useful context.
if (render_frame_host &&
render_frame_host->IsInLifecycleState(
content::RenderFrameHost::LifecycleState::kInBackForwardCache)) {
// The ExtensionMessagePort should be disconnected when the page enters
// BFCache if `kDisconnectExtensionMessagePortWhenPageEntersBFCache` is
// enabled, so no message will be sent to the BFCached target. There could
// be some messages that were created before the ExtensionMessagePort is
// disconnected, and they should be discarded.
// TODO(crbug.com/1488379): clean up the flag.
if (!base::FeatureList::IsEnabled(
features::kDisconnectExtensionMessagePortWhenPageEntersBFCache)) {
content::BackForwardCache::DisableForRenderFrameHost(
render_frame_host,
back_forward_cache::DisabledReason(
back_forward_cache::DisabledReasonId::
kExtensionSentMessageToCachedFrame,
/*context=*/extension_id_),
ukm::UkmRecorder::GetSourceIdForExtensionUrl(
base::PassKey<ExtensionMessagePort>(),
Extension::GetBaseURLFromExtensionId(extension_id_)));
}
return true;
}
return false;
}
} // namespace extensions