// Copyright 2013 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/process_map.h"

#include <algorithm>
#include <string>
#include <tuple>

#include "base/containers/map_util.h"
#include "base/types/optional_util.h"
#include "components/guest_view/buildflags/buildflags.h"
#include "content/public/browser/child_process_security_policy.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/common/child_process_id.h"
#include "content/public/common/url_constants.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/process_map_factory.h"
#include "extensions/browser/script_injection_tracker.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_id.h"
#include "extensions/common/features/feature.h"
#include "extensions/common/mojom/context_type.mojom.h"
#include "pdf/buildflags.h"

#if BUILDFLAG(ENABLE_GUEST_VIEW)
#include "extensions/browser/guest_view/web_view/web_view_renderer_state.h"
#endif

#if BUILDFLAG(ENABLE_PDF)
#include "extensions/common/constants.h"
#include "pdf/pdf_features.h"
#endif

namespace extensions {

namespace {

// Returns true if `process_id` is associated with a WebUI process.
bool ProcessHasWebUIBindings(content::ChildProcessId process_id) {
  // TODO(crbug.com/40676401): HasWebUIBindings does not always return true for
  // WebUIs. This should be changed to use something else.
  // TODO(crbug.com/379869738) Remove GetUnsafeValue.
  return content::ChildProcessSecurityPolicy::GetInstance()->HasWebUIBindings(
      process_id.GetUnsafeValue());
}

// Returns true if `process_id` is associated with a webview owned by the
// extension with the specified `extension_id`.
bool IsWebViewProcessForExtension(content::ChildProcessId process_id,
                                  const ExtensionId& extension_id) {
#if BUILDFLAG(ENABLE_GUEST_VIEW)
  WebViewRendererState* web_view_state = WebViewRendererState::GetInstance();
  // TODO(crbug.com/379869738) Remove GetUnsafeValue.
  if (!web_view_state->IsGuest(process_id.GetUnsafeValue())) {
    return false;
  }

  std::string webview_owner;
  int owner_process_id = -1;
  // TODO(crbug.com/379869738) Remove GetUnsafeValue.
  bool found_info = web_view_state->GetOwnerInfo(
      process_id.GetUnsafeValue(), &owner_process_id, &webview_owner);
  return found_info && webview_owner == extension_id;
#else
  return false;
#endif
}

}  // namespace

// ProcessMap
ProcessMap::ProcessMap(content::BrowserContext* browser_context)
    : browser_context_(browser_context) {}

ProcessMap::~ProcessMap() = default;

void ProcessMap::Shutdown() {
  browser_context_ = nullptr;
}

// static
ProcessMap* ProcessMap::Get(content::BrowserContext* browser_context) {
  return ProcessMapFactory::GetForBrowserContext(browser_context);
}

bool ProcessMap::Insert(const ExtensionId& extension_id,
                        content::ChildProcessId process_id) {
  return items_.emplace(process_id, extension_id).second;
}

int ProcessMap::Remove(content::ChildProcessId process_id) {
  return items_.erase(process_id);
}

bool ProcessMap::Contains(const ExtensionId& extension_id_in,
                          content::ChildProcessId process_id) const {
  auto* extension_id = base::FindOrNull(items_, process_id);
  return extension_id && *extension_id == extension_id_in;
}

bool ProcessMap::Contains(content::ChildProcessId process_id) const {
  return items_.contains(process_id);
}

bool ProcessMap::Contains(int process_id) const {
  return Contains(content::ChildProcessId::FromUnsafeValue(process_id));
}

bool ProcessMap::ExtensionHasProcess(const ExtensionId& extension_id) const {
  return std::ranges::find_if(items_, [extension_id](const auto& entry) {
           return entry.second == extension_id;
         }) != items_.end();
}

const Extension* ProcessMap::GetEnabledExtensionByProcessID(
    content::ChildProcessId process_id) const {
  auto* extension_id = base::FindOrNull(items_, process_id);
  return extension_id ? ExtensionRegistry::Get(browser_context_)
                            ->enabled_extensions()
                            .GetByID(*extension_id)
                      : nullptr;
}

std::optional<ExtensionId> ProcessMap::GetExtensionIdForProcess(
    content::ChildProcessId process_id) const {
  return base::OptionalFromPtr(base::FindOrNull(items_, process_id));
}

bool ProcessMap::IsPrivilegedExtensionProcess(
    const Extension& extension,
    content::ChildProcessId process_id) {
  return Contains(extension.id(), process_id) &&
         // Hosted apps aren't considered privileged extension processes...
         (!extension.is_hosted_app() ||
          // ... Unless they're component hosted apps, like the webstore.
          // TODO(https://crbug/1429667): We can clean this up when we remove
          // special handling of component hosted apps.
          extension.location() == mojom::ManifestLocation::kComponent);
}

bool ProcessMap::CanProcessHostContextType(
    const Extension* extension,
    const content::RenderProcessHost& process,
    mojom::ContextType context_type) {
  const content::ChildProcessId process_id = process.GetID();
  switch (context_type) {
    case mojom::ContextType::kUnspecified:
      // We never consider unspecified contexts valid. Even though they would be
      // permissionless, they should never be able to make a request to the
      // browser.
      return false;
    case mojom::ContextType::kOffscreenExtension:
    case mojom::ContextType::kPrivilegedExtension:
      // Offscreen documents run in the main extension process, so both of these
      // require a privileged extension process.
      return extension && IsPrivilegedExtensionProcess(*extension, process_id);
    case mojom::ContextType::kUnprivilegedExtension:
      return extension &&
             IsWebViewProcessForExtension(process_id, extension->id());
    case mojom::ContextType::kContentScript:
      return extension &&
             ScriptInjectionTracker::DidProcessRunContentScriptFromExtension(
                 process, extension->id());
    case mojom::ContextType::kUserScript:
      return extension &&
             ScriptInjectionTracker::DidProcessRunUserScriptFromExtension(
                 process, extension->id());
    case mojom::ContextType::kPrivilegedWebPage:
      // A privileged web page is a (non-component) hosted app process.
      return extension && extension->is_hosted_app() &&
             extension->location() != mojom::ManifestLocation::kComponent &&
             Contains(extension->id(), process_id);
    case mojom::ContextType::kUntrustedWebUi:
      // Unfortunately, we have no way of checking if a *process* can host
      // untrusted webui contexts. Callers should look at (ideally, the
      // browser-verified) origin.
      [[fallthrough]];
    case mojom::ContextType::kWebPage:
      // Any context not associated with an extension, not running in an
      // extension process, and without webui bindings can be considered a
      // web page process.
      return !extension && !Contains(process_id) &&
             !ProcessHasWebUIBindings(process_id);
    case mojom::ContextType::kWebUi:
      // Don't consider extensions in webui (like content scripts) to be
      // webui.
      return !extension && ProcessHasWebUIBindings(process_id);
  }
}

mojom::ContextType ProcessMap::GetMostLikelyContextType(
    const Extension* extension,
    content::ChildProcessId process_id,
    const GURL* url) const {
  // WARNING: This logic must match ScriptContextSet::ClassifyJavaScriptContext,
  // as much as possible.

  // TODO(crbug.com/40676105): Move this into the !extension if statement below
  // or document why we want to return WEBUI_CONTEXT for content scripts in
  // WebUIs.
  if (ProcessHasWebUIBindings(process_id)) {
    return mojom::ContextType::kWebUi;
  }

  if (!extension) {
    // Note that blob/filesystem schemes associated with an inner URL of
    // chrome-untrusted will be considered regular pages.
    if (url && url->SchemeIs(content::kChromeUIUntrustedScheme)) {
      return mojom::ContextType::kUntrustedWebUi;
    }

    return mojom::ContextType::kWebPage;
  }

  const ExtensionId& extension_id = extension->id();
  if (!Contains(extension_id, process_id)) {
    // If the process map doesn't contain the process, it might be an extension
    // frame in a webview.
    // We (deliberately) don't add webview-hosted frames to the process map and
    // don't classify them as kPrivilegedExtension contexts.
    if (url && extension->origin().IsSameOriginWith(*url) &&
        IsWebViewProcessForExtension(process_id, extension->id())) {
      // Yep, it's an extension frame in a webview.
#if BUILDFLAG(ENABLE_PDF)
      // The PDF Viewer extension is an exception, since webviews need to be
      // able to load the PDF Viewer. The PDF extension needs a
      // kPrivilegedExtension context to load, so the PDF extension frame is
      // added to the process map and shouldn't reach here.
      if (chrome_pdf::features::IsOopifPdfEnabled()) {
        CHECK_NE(extension_id, extension_misc::kPdfExtensionId);
      }
#endif  // BUILDFLAG(ENABLE_PDF)

      return mojom::ContextType::kUnprivilegedExtension;
    }

    // Otherwise, it's a content script (the context in which an extension can
    // run in an unassociated, non-webview process).
    return mojom::ContextType::kContentScript;
  }

  if (extension->is_hosted_app() &&
      extension->location() != mojom::ManifestLocation::kComponent) {
    return mojom::ContextType::kPrivilegedWebPage;
  }

  // TODO(crbug.com/40849649): Currently, offscreen document contexts
  // are misclassified as kPrivilegedExtension contexts. This is not ideal
  // because there is a mismatch between the browser and the renderer), but it's
  // not a security issue because, while offscreen documents have fewer
  // capabilities, this is an API distinction, and not a security enforcement.
  // Offscreen documents run in the same process as the rest of the extension
  // and can message the extension, so could easily - though indirectly -
  // access all the same features.
  // Even so, we should fix this to properly classify offscreen documents (and
  // this would be a problem if offscreen documents ever have access to APIs
  // that kPrivilegedExtension contexts don't).

  return mojom::ContextType::kPrivilegedExtension;
}

}  // namespace extensions
