#include "extensions/browser/events/event_dispatch_helper.h"
#include <optional>
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/task/sequenced_task_runner.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 {
bool CrossesIncognito(const BrowserContext& context, const Event& event) {
return event.restrict_to_browser_context &&
&context != event.restrict_to_browser_context;
}
bool CanDispatchEventToBrowserContext(BrowserContext& context,
const Extension* extension,
const Event& event) {
bool crosses_incognito = CrossesIncognito(context, event);
if (!crosses_incognito) {
return true;
}
return ExtensionsBrowserClient::Get()->CanExtensionCrossIncognito(extension,
&context);
}
bool CheckPermissions(const Extension* extension,
const Event& event,
BrowserContext& listener_context,
mojom::ContextType target_context_type) {
if (extension) {
if (!event.event_url.is_empty() &&
event.event_url.GetHost() != extension->id() &&
!extension->permissions_data()
->active_permissions()
.HasEffectiveAccessToURL(event.event_url)) {
return false;
}
if (!CanDispatchEventToBrowserContext(listener_context, extension, event)) {
return false;
}
} else {
if (CrossesIncognito(listener_context, event)) {
return false;
}
}
if (event.restrict_to_context_type.has_value() &&
event.restrict_to_context_type.value() != target_context_type) {
return false;
}
return true;
}
}
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;
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));
}
bool EventDispatchHelper::CheckFeatureAvailability(
const Event& event,
const Extension* extension,
const GURL& listener_url,
content::RenderProcessHost& process,
BrowserContext& listener_context,
mojom::ContextType target_context_type) {
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()) {
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));
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);
}
}
if (!contexts_pending_dispatch_.empty() && event->cannot_dispatch_callback) {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, std::move(event->cannot_dispatch_callback));
}
}
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;
}
TryQueueEventForLazyListener(
event, LazyContextIdForListener(listener, *browser_context_),
listener->filter());
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;
}
content::RenderProcessHost* process = listener->process();
BrowserContext* listener_context = process->GetBrowserContext();
auto lazy_context_id = LazyContextIdForListener(listener, *listener_context);
if (IsAlreadyQueued(lazy_context_id)) {
return;
}
contexts_pending_dispatch_.erase(lazy_context_id);
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;
}
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) {
return;
}
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), 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;
}
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;
}
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)) {
contexts_pending_dispatch_.insert(dispatch_context);
return false;
}
std::unique_ptr<Event> dispatched_event = CreateEventForDispatch(
event, listener_filter, extension, *browser_context, context_type,
nullptr);
if (!dispatched_event) {
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();
}
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)) {
return nullptr;
}
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(
!is_event_args_modified,
!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());
}
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);
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);
}
}