910e62b5创建于 1月15日历史提交
// 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 "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 {

// 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);
    }
  }

  // NOTE: this code mirrors the logic in `EventRouter::DispatchPendingEvent`.
  if (!contexts_pending_dispatch_.empty() && event->cannot_dispatch_callback) {
    // Even though a context was active, there was no registered listener
    // associated with this event. This can happen if an extension
    // asynchronously registers event listeners. In this case, notify the caller
    // (if they subscribed via a callback) and drop the event.
    //
    // NOTE: we need to post a task rather than just executing the callback,
    // because the callback can rely on state that will be setup in the current
    // sequence, e.g. `WebRequestEventRouter::OnHeadersReceived` sets up a
    // `BlockedRequest` that has to be processed. Failure to do this can cause
    // the browser to hang for `webRequestBlocking` events.
    // See crbug.com/467448815.
    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;
  }

  // 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();

  auto lazy_context_id = LazyContextIdForListener(listener, *listener_context);
  if (IsAlreadyQueued(lazy_context_id)) {
    return;
  }
  // The task wasn't queued in `DispatchEventToLazyListener` in expectation for
  // dispatch to this active listener. We are about to process that dispatch,
  // so we can remove the context from the set of pending ones.
  contexts_pending_dispatch_.erase(lazy_context_id);

  // 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)) {
    // Keep track of contexts for which we decided not to enqueue the task,
    // because the context was active. We expect to dispatch later to the
    // corresponding non-lazy listener.
    contexts_pending_dispatch_.insert(dispatch_context);
    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