// 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/renderer/resource_request_policy.h"

#include <string_view>

#include "base/logging.h"
#include "base/strings/stringprintf.h"
#include "base/types/optional_util.h"
#include "content/public/common/url_constants.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension.h"
#include "extensions/common/manifest_constants.h"
#include "extensions/common/manifest_handlers/icons_handler.h"
#include "extensions/common/manifest_handlers/web_accessible_resources_info.h"
#include "extensions/common/manifest_handlers/webview_info.h"
#include "extensions/renderer/dispatcher.h"
#include "pdf/buildflags.h"
#include "third_party/blink/public/platform/url_conversion.h"
#include "third_party/blink/public/platform/web_string.h"
#include "third_party/blink/public/platform/web_url.h"
#include "third_party/blink/public/web/web_console_message.h"
#include "third_party/blink/public/web/web_document.h"
#include "third_party/blink/public/web/web_local_frame.h"
#include "ui/base/page_transition_types.h"
#include "url/gurl.h"
#include "url/origin.h"

namespace extensions {

ResourceRequestPolicy::ResourceRequestPolicy(Dispatcher* dispatcher,
                                             std::unique_ptr<Delegate> delegate)
    : dispatcher_(dispatcher), delegate_(std::move(delegate)) {}
ResourceRequestPolicy::~ResourceRequestPolicy() = default;

void ResourceRequestPolicy::OnExtensionLoaded(const Extension& extension) {
  if (WebAccessibleResourcesInfo::HasWebAccessibleResources(&extension) ||
      WebviewInfo::HasWebviewAccessibleResources(
          extension,
          dispatcher_->webview_partition_id().value_or(std::string())) ||
      // Hosted app icons are accessible.
      // TODO(devlin): Should we incorporate this into
      // WebAccessibleResourcesInfo?
      (extension.is_hosted_app() && !IconsInfo::GetIcons(&extension).empty())) {
    web_accessible_resources_map_[extension.id()] = extension.guid();
  }
}

bool ResourceRequestPolicy::IsWebAccessibleHost(const std::string& host) {
  if (web_accessible_resources_map_.find(host) !=
      web_accessible_resources_map_.end()) {
    return true;
  }
  for (const auto& [id, guid] : web_accessible_resources_map_) {
    if (host == guid) {
      return true;
    }
  }
  return false;
}

void ResourceRequestPolicy::OnExtensionUnloaded(
    const ExtensionId& extension_id) {
  web_accessible_resources_map_.erase(extension_id);
}

// This method does a security check whether chrome-extension:// URLs can be
// requested by the renderer. Since this is in an untrusted process, the browser
// has a similar check to enforce the policy, in case this process is exploited.
// If you are changing this function, ensure equivalent checks are added to
// extension_protocols.cc's AllowExtensionResourceLoad.
bool ResourceRequestPolicy::CanRequestResource(
    const GURL& upstream_url,
    const GURL& target_url,
    blink::WebLocalFrame* frame,
    ui::PageTransition transition_type,
    const url::Origin* initiator_origin) {
  // `target_url` is expected to have a chrome-extension scheme.
  // `upstream_url` could be empty, have a chrome-extension scheme, or other.
  CHECK(target_url.SchemeIs(kExtensionScheme));

  GURL frame_url = frame->GetDocument().Url();
  url::Origin frame_origin = frame->GetDocument().GetSecurityOrigin();

  // Navigations from chrome://, devtools://, or embedder-specified pages need
  // to be allowed, even if the target |url| is not web-accessible.  See also:
  // - https://crbug.com/41284772
  // - similar scheme checks in ExtensionNavigationThrottle
  if (frame_origin.scheme() == content::kChromeUIScheme ||
      frame_origin.scheme() == content::kChromeDevToolsScheme) {
    return true;
  }
  if (delegate_ &&
      delegate_->ShouldAlwaysAllowRequestForFrameOrigin(frame_origin)) {
    return true;
  }

  // The page_origin may be GURL("null") for unique origins like data URLs,
  // but this is ok for the checks below.  We only care if it matches the
  // current extension or has a devtools scheme.
  GURL page_origin = url::Origin(frame->Top()->GetSecurityOrigin()).GetURL();

  GURL extension_origin = target_url.DeprecatedGetOriginAsURL();

  // We always allow loads in the following cases, regardless of web accessible
  // resources:

  // Empty urls (needed for some edge cases when we have empty urls).
  if (frame_url.is_empty()) {
    return true;
  }

  // Extensions requesting their own resources (frame_url check is for images,
  // page_url check is for iframes).
  // TODO(devlin): We should be checking the ancestor chain, not just the
  // top-level frame. Additionally, we should be checking the security origin
  // of the frame, to account for about:blank subframes being scripted by an
  // extension parent (though we'll still need the frame origin check for
  // sandboxed frames).
  if (frame_url.DeprecatedGetOriginAsURL() == extension_origin ||
      page_origin == extension_origin) {
    return true;
  }

  if (!ui::PageTransitionIsWebTriggerable(transition_type)) {
    return true;
  }

  // Unreachable web page error page (to allow showing the icon of the
  // unreachable app on this page).
  if (frame_url == content::kUnreachableWebDataURL) {
    return true;
  }

#if BUILDFLAG(ENABLE_PDF)
  // Handle specific cases for the PDF viewer.
  if (extension_origin.GetScheme() == kExtensionScheme &&
      extension_origin.GetHost() == extension_misc::kPdfExtensionId) {
    // For the PDF viewer, `page_origin` doesn't match the `extension_origin`,
    // but the PDF extension frame should still be able to request resources
    // from itself. The PDF content frame should also be able to request
    // resources from the PDF extension. For both cases, the parent origin of
    // the current frame matches the extension origin.
    blink::WebFrame* parent = frame->Parent();
    if (parent) {
      GURL parent_origin = url::Origin(parent->GetSecurityOrigin()).GetURL();
      if (parent_origin == extension_origin) {
        return true;
      }
    }
  }
#endif  // BUILDFLAG(ENABLE_PDF)

  if (delegate_ &&
      delegate_->AllowLoadForDevToolsPage(page_origin, target_url)) {
    return true;
  }

  // Note: we check |web_accessible_ids_| (rather than first looking up the
  // extension in the registry and checking that) to be more resistant against
  // timing attacks. This way, determining access for an extension that isn't
  // installed takes the same amount of time as determining access for an
  // extension with no web accessible resources. We aren't worried about any
  // extensions with web accessible resources, since those are inherently
  // identifiable.
  if (!IsWebAccessibleHost(extension_origin.GetHost())) {
    return false;
  }

  const Extension* extension =
      RendererExtensionRegistry::Get()->GetExtensionOrAppByURL(
          target_url, true /*include_guid*/);
  DCHECK(extension);

  // Disallow loading of packaged resources for hosted apps. We don't allow
  // hybrid hosted/packaged apps. The one exception is access to icons, since
  // some extensions want to be able to do things like create their own
  // launchers.
  std::string_view resource_root_relative_path =
      target_url.path().empty() ? std::string_view()
                                : target_url.path().substr(1);
  if (extension->is_hosted_app() &&
      !IconsInfo::GetIcons(extension).ContainsPath(
          resource_root_relative_path)) {
    LOG(ERROR) << "Denying load of " << target_url.spec() << " from "
               << "hosted app.";
    return false;
  }

  // Disallow loading of extension resources which are not explicitly listed
  // as web or WebView accessible if the manifest version is 2 or greater.
  // `upstream_url` might be set to GURL() in some cases, if it's not available.
  auto opt_initiator_origin = base::OptionalFromPtr(initiator_origin);
  if (!WebAccessibleResourcesInfo::IsResourceWebAccessibleRedirect(
          extension, target_url, opt_initiator_origin, upstream_url) &&
      !WebviewInfo::IsResourceWebviewAccessible(
          extension,
          dispatcher_->webview_partition_id().value_or(std::string()),
          target_url.GetPath())) {
    std::string message = base::StringPrintf(
        "Denying load of %s. Resources must be listed in the "
        "web_accessible_resources manifest key in order to be loaded by "
        "pages outside the extension.",
        target_url.spec().c_str());
    frame->AddMessageToConsole(
        blink::WebConsoleMessage(blink::mojom::ConsoleMessageLevel::kError,
                                 blink::WebString::FromUtf8(message)));
    return false;
  }

  return true;
}

}  // namespace extensions
