blob: e02cb0060a9ebcfd30075edc420f3c021bac410a [file] [log] [blame]
// Copyright 2025 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/events/event_dispatch_helper.h"
#include <optional>
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "content/public/browser/render_process_host.h"
#include "extensions/browser/browser_process_context_data.h"
#include "extensions/browser/event_router.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_util.h"
#include "extensions/browser/extensions_browser_client.h"
#include "extensions/browser/lazy_context_id.h"
#include "extensions/browser/process_map.h"
#include "extensions/common/extension_api.h"
#include "extensions/common/features/feature.h"
#include "extensions/common/manifest_handlers/background_info.h"
#include "extensions/common/manifest_handlers/incognito_info.h"
#include "extensions/common/mojom/context_type.mojom.h"
#include "extensions/common/mojom/event_dispatcher.mojom.h"
#include "extensions/common/permissions/permissions_data.h"
using content::BrowserContext;
namespace extensions {
namespace {
// Returns whether an event would cross the incognito boundary. e.g.
// incognito->regular or regular->incognito. This is allowed for some extensions
// that enable spanning-mode but is always disallowed for webUI.
// `context` refers to the BrowserContext of the receiver of the event.
bool CrossesIncognito(const BrowserContext& context, const Event& event) {
return event.restrict_to_browser_context &&
&context != event.restrict_to_browser_context;
}
// Returns false when the event is scoped to a context and the listening
// extension does not have access to events from that context.
bool CanDispatchEventToBrowserContext(BrowserContext& context,
const Extension* extension,
const Event& event) {
// Is this event from a different browser context than the renderer (ie, an
// incognito tab event sent to a normal process, or vice versa).
bool crosses_incognito = CrossesIncognito(context, event);
if (!crosses_incognito) {
return true;
}
return ExtensionsBrowserClient::Get()->CanExtensionCrossIncognito(extension,
&context);
}
// Returns true if the listener has permission to receive the given `event`.
//
// For extensions, this checks for host permissions to the event's URL and
// whether the extension can receive events from the event's browser context
// (e.g., an incognito event sent to a regular process).
//
// For non-extension listeners (e.g., WebUI), this primarily checks that the
// event does not cross the incognito boundary.
bool CheckPermissions(const Extension* extension,
const Event& event,
BrowserContext& listener_context,
mojom::ContextType target_context_type) {
if (extension) {
// Extension-specific checks.
// Firstly, if the event is for a URL, the Extension must have permission
// to access that URL.
if (!event.event_url.is_empty() &&
event.event_url.GetHost() != extension->id() && // event for self is ok
!extension->permissions_data()
->active_permissions()
.HasEffectiveAccessToURL(event.event_url)) {
return false;
}
// Secondly, if the event is for incognito mode, the Extension must be
// enabled in incognito mode.
if (!CanDispatchEventToBrowserContext(listener_context, extension, event)) {
return false;
}
} else {
// Non-extension (e.g. WebUI and web pages) checks. In general we don't
// allow context-bound events to cross the incognito barrier.
if (CrossesIncognito(listener_context, event)) {
return false;
}
}
// Don't dispatch an event when target context doesn't match the restricted
// context type.
if (event.restrict_to_context_type.has_value() &&
event.restrict_to_context_type.value() != target_context_type) {
return false;
}
return true;
}
} // namespace
EventDispatchHelper::EventDispatchHelper(
const ExtensionRegistry& extension_registry,
BrowserContext& browser_context,
EventListenerMap& listeners,
DispatchFunction dispatch_function,
DispatchToProcessFunction dispatch_to_process_function)
: extension_registry_(extension_registry),
browser_context_(browser_context),
listeners_(listeners),
dispatch_function_(std::move(dispatch_function)),
dispatch_to_process_function_(std::move(dispatch_to_process_function)) {}
EventDispatchHelper::~EventDispatchHelper() = default;
// static
void EventDispatchHelper::DispatchEvent(
content::BrowserContext& browser_context,
EventListenerMap& listeners,
DispatchFunction dispatch_function,
DispatchToProcessFunction dispatch_to_process_function,
const ExtensionId& restrict_to_extension_id,
const GURL& restrict_to_url,
std::unique_ptr<Event> event) {
const ExtensionRegistry* extension_registry =
ExtensionRegistry::Get(&browser_context);
DCHECK(extension_registry);
EventDispatchHelper(*extension_registry, browser_context, listeners,
dispatch_function, dispatch_to_process_function)
.DispatchEventImpl(restrict_to_extension_id, restrict_to_url,
std::move(event));
}
// static
bool EventDispatchHelper::CheckFeatureAvailability(
const Event& event,
const Extension* extension,
const GURL& listener_url,
content::RenderProcessHost& process,
BrowserContext& listener_context,
mojom::ContextType target_context_type) {
// We shouldn't be dispatching an event to a webpage, since all such events
// (e.g. messaging) don't go through EventRouter. The exceptions to this are
// the new chrome webstore domain, which has permission to receive extension
// events and features with delegated availability checks, such as Controlled
// Frame which runs within Isolated Web Apps and appear as web pages.
Feature::Availability availability =
ExtensionAPI::GetSharedInstance()->IsAvailable(
event.event_name, extension, target_context_type, listener_url,
CheckAliasStatus::ALLOWED,
util::GetBrowserContextId(&listener_context),
BrowserProcessContextData(&process));
if (!availability.is_available()) {
// TODO(crbug.com/40255138): Ideally it shouldn't be possible to reach here,
// because access is checked on registration. However, we don't always
// refresh the list of events an extension has registered when other factors
// which affect availability change (e.g. API allowlists changing). Those
// situations should be identified and addressed.
return false;
}
return true;
}
void EventDispatchHelper::DispatchEventImpl(
const ExtensionId& restrict_to_extension_id,
const GURL& restrict_to_url,
std::unique_ptr<Event> event) {
std::set<const EventListener*> listeners(
listeners_->GetEventListeners(*event));
// We dispatch events for lazy background pages first because attempting to do
// so will cause those that are being suspended to cancel that suspension.
// As canceling a suspension entails sending an event to the affected
// background page, and as that event needs to be delivered before we dispatch
// the event we are dispatching here, we dispatch to the lazy listeners here
// first.
for (const EventListener* listener : listeners) {
if (listener->IsLazy()) {
DispatchEventToLazyListener(restrict_to_extension_id, restrict_to_url,
*event, listener);
}
}
for (const EventListener* listener : listeners) {
if (!listener->IsLazy()) {
DispatchEventToActiveListener(restrict_to_extension_id, restrict_to_url,
*event, listener);
}
}
}
void EventDispatchHelper::DispatchEventToLazyListener(
const ExtensionId& restrict_to_extension_id,
const GURL& restrict_to_url,
Event& event,
const EventListener* listener) {
DCHECK(listener->IsLazy());
if (!ListenerMeetsRestrictions(listener, restrict_to_extension_id,
restrict_to_url)) {
return;
}
// Lazy listeners don't have a process, take the stored browser context
// for lazy context.
TryQueueEventForLazyListener(
event, LazyContextIdForListener(listener, *browser_context_),
listener->filter());
// Dispatch to lazy listener in the incognito context.
// We need to use the incognito context in the case of split-mode
// extensions.
BrowserContext* incognito_context =
GetIncognitoContextIfAccessible(listener->extension_id());
if (incognito_context) {
TryQueueEventForLazyListener(
event, LazyContextIdForListener(listener, *incognito_context),
listener->filter());
}
}
void EventDispatchHelper::DispatchEventToActiveListener(
const ExtensionId& restrict_to_extension_id,
const GURL& restrict_to_url,
const Event& event,
const EventListener* listener) {
DCHECK(!listener->IsLazy());
if (!ListenerMeetsRestrictions(listener, restrict_to_extension_id,
restrict_to_url)) {
return;
}
// Non-lazy listeners take the process browser context for context.
content::RenderProcessHost* process = listener->process();
BrowserContext* listener_context = process->GetBrowserContext();
if (IsAlreadyQueued(LazyContextIdForListener(listener, *listener_context))) {
return;
}
// Determine the target context type.
ProcessMap* listener_process_map = ProcessMap::Get(listener_context);
const Extension* extension = GetExtension(listener->extension_id());
const GURL* url = listener->service_worker_version_id() ==
blink::mojom::kInvalidServiceWorkerVersionId
? &listener->listener_url()
: nullptr;
auto context_type = listener_process_map->GetMostLikelyContextType(
extension, process->GetDeprecatedID(), url);
if (!CheckPermissions(extension, event, *listener_context, context_type) ||
!CheckFeatureAvailability(event, extension, listener->listener_url(),
*process, *listener_context, context_type)) {
return;
}
// Prepare event for dispatch, running the `will_dispatch_callback` if any.
bool dispatch_separate_event = true;
std::unique_ptr<Event> dispatched_event = CreateEventForDispatch(
event, listener->filter(), extension, *listener_context, context_type,
&dispatch_separate_event);
if (!dispatched_event) {
// The event has been canceled.
return;
}
// Check if we've already dispatched this event to this active context.
// If multiple listeners match the same event within the same active context,
// we only dispatch the event once, provided de-duplication is enabled
// (i.e., `dispatch_separate_event` is true, indicating arguments are expected
// to be consistent across listeners).
auto [_, inserted] = dispatched_active_ids_.emplace(
ActiveContextId{.render_process = process,
.worker_thread_id = listener->worker_thread_id(),
.extension_id = listener->extension_id(),
.browser_context = listener_context,
.listener_url = listener->listener_url()});
if (dispatch_separate_event && !inserted) {
return;
}
dispatch_to_process_function_.Run(
listener->extension_id(), listener->listener_url(), process,
listener->service_worker_version_id(), listener->worker_thread_id(),
std::move(dispatched_event), /*did_enqueue=*/false);
}
void EventDispatchHelper::TryQueueEventForLazyListener(
Event& event,
const LazyContextId& dispatch_context,
const base::Value::Dict* listener_filter) {
const Extension* extension = GetExtension(dispatch_context.extension_id());
if (!extension) {
return;
}
// Check both the browser context to see if we should load a
// non-persistent context (a lazy background page or an extension
// service worker) to handle the event.
if (TryQueueEventDispatch(event, dispatch_context, extension,
listener_filter)) {
RecordAlreadyQueued(dispatch_context);
}
}
bool EventDispatchHelper::TryQueueEventDispatch(
Event& event,
const LazyContextId& dispatch_context,
const Extension* extension,
const base::Value::Dict* listener_filter) {
if (IsAlreadyQueued(dispatch_context)) {
return false;
}
// The only lazy listeners belong to an extension's background context (either
// an event page or a service worker), which are always kPrivilegedExtension
// contexts.
auto context_type = mojom::ContextType::kPrivilegedExtension;
BrowserContext* browser_context = dispatch_context.browser_context();
if (!CheckPermissions(extension, event, *browser_context, context_type)) {
return false;
}
LazyContextTaskQueue* queue = dispatch_context.GetTaskQueue();
event.lazy_background_active_on_dispatch =
queue->IsReadyToRunTasks(browser_context, extension);
if (!queue->ShouldEnqueueTask(browser_context, extension)) {
return false;
}
// Prepare the event for dispatch, running the `will_dispatch_callback` if
// any. We do this now (rather than dispatch time) to avoid lifetime issues.
std::unique_ptr<Event> dispatched_event = CreateEventForDispatch(
event, listener_filter, extension, *browser_context, context_type,
/*dispatch_separate_event_out=*/nullptr);
if (!dispatched_event) {
// The event has been canceled.
return true;
}
queue->AddPendingTask(
dispatch_context,
base::BindOnce(dispatch_function_, std::move(dispatched_event)));
return true;
}
std::unique_ptr<Event> EventDispatchHelper::CreateEventForDispatch(
const Event& event,
const base::Value::Dict* listener_filter,
const Extension* extension,
BrowserContext& listener_context,
mojom::ContextType target_context_type,
bool* dispatch_separate_event_out) {
if (event.will_dispatch_callback.is_null()) {
return event.DeepCopy();
}
// Run the callback before copying the event to determine if events need
// de-duplicating based on the `dispatch_separate_event_out` argument.
std::optional<base::Value::List> modified_event_args;
mojom::EventFilteringInfoPtr modified_event_filter_info;
if (!event.will_dispatch_callback.Run(
&listener_context, target_context_type, extension, listener_filter,
modified_event_args, modified_event_filter_info,
dispatch_separate_event_out)) {
// The event has been canceled.
return nullptr;
}
// If `event_args` or `filter_info` are modified, we avoid cloning the
// original ones (which can be costly) by using a selective copy mechanism.
const bool is_event_args_modified = modified_event_args.has_value();
const bool is_filter_info_modified = !!modified_event_filter_info;
std::unique_ptr<Event> dispatched_event = event.CopySelectively(
/*copy_event_args=*/!is_event_args_modified,
/*copy_filter_info=*/!is_filter_info_modified);
if (is_event_args_modified) {
dispatched_event->event_args = std::move(*modified_event_args);
}
if (is_filter_info_modified) {
dispatched_event->filter_info = std::move(modified_event_filter_info);
}
dispatched_event->will_dispatch_callback.Reset();
return dispatched_event;
}
void EventDispatchHelper::RecordAlreadyQueued(
const LazyContextId& dispatch_context) {
dispatched_ids_.insert(dispatch_context);
}
bool EventDispatchHelper::IsAlreadyQueued(
const LazyContextId& dispatch_context) const {
return base::Contains(dispatched_ids_, dispatch_context);
}
bool EventDispatchHelper::ListenerMeetsRestrictions(
const EventListener* listener,
const ExtensionId& restrict_to_extension_id,
const GURL& restrict_to_url) const {
if (!restrict_to_extension_id.empty() &&
restrict_to_extension_id != listener->extension_id()) {
return false;
}
if (!restrict_to_url.is_empty() &&
!url::IsSameOriginWith(restrict_to_url, listener->listener_url())) {
return false;
}
return true;
}
BrowserContext* EventDispatchHelper::GetIncognitoContextIfAccessible(
const ExtensionId& extension_id) const {
DCHECK(!extension_id.empty());
const Extension* extension = GetExtension(extension_id);
if (!extension) {
return nullptr;
}
if (!IncognitoInfo::IsSplitMode(extension)) {
return nullptr;
}
if (!util::IsIncognitoEnabled(extension_id, &browser_context_.get())) {
return nullptr;
}
return GetIncognitoContext();
}
BrowserContext* EventDispatchHelper::GetIncognitoContext() const {
ExtensionsBrowserClient* browser_client = ExtensionsBrowserClient::Get();
if (!browser_client->HasOffTheRecordContext(&browser_context_.get())) {
return nullptr;
}
return browser_client->GetOffTheRecordContext(&browser_context_.get());
}
// Browser context is required for lazy context id. Before adding browser
// context member to EventListener, callers must pass in the browser context as
// a parameter.
// TODO(richardzh): Once browser context is added as a member to EventListener,
// update this method to get browser_context from listener
// instead of parameter.
LazyContextId EventDispatchHelper::LazyContextIdForListener(
const EventListener* listener,
BrowserContext& browser_context) const {
const Extension* extension = GetExtension(listener->extension_id());
const bool is_service_worker_based_extension =
extension && BackgroundInfo::IsServiceWorkerBased(extension);
// Note: It is possible that the prefs' listener->is_for_service_worker() and
// its extension background type do not agree. This happens when one changes
// extension's manifest, typically during unpacked extension development.
// Fallback to non-Service worker based LazyContextId to avoid surprising
// ServiceWorkerTaskQueue (and crashing), see https://crbug.com/1239752 for
// details.
// TODO(lazyboy): Clean these inconsistencies across different types of event
// listener and their corresponding background types.
if (is_service_worker_based_extension && listener->is_for_service_worker()) {
return LazyContextId::ForServiceWorker(&browser_context,
listener->extension_id());
}
return LazyContextId::ForBackgroundPage(&browser_context,
listener->extension_id());
}
const Extension* EventDispatchHelper::GetExtension(
const ExtensionId& extension_id) const {
return extension_registry_->enabled_extensions().GetByID(extension_id);
}
} // namespace extensions