// Copyright 2017 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/api/declarative_net_request/ruleset_manager.h"

#include <algorithm>
#include <iterator>
#include <optional>
#include <tuple>

#include "base/check_op.h"
#include "base/command_line.h"
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/notreached.h"
#include "base/time/time.h"
#include "base/timer/elapsed_timer.h"
#include "components/web_cache/browser/web_cache_manager.h"
#include "extensions/browser/api/declarative_net_request/composite_matcher.h"
#include "extensions/browser/api/declarative_net_request/constants.h"
#include "extensions/browser/api/declarative_net_request/request_action.h"
#include "extensions/browser/api/declarative_net_request/request_params.h"
#include "extensions/browser/api/declarative_net_request/utils.h"
#include "extensions/browser/api/declarative_webrequest/request_stage.h"
#include "extensions/browser/api/extensions_api_client.h"
#include "extensions/browser/api/web_request/permission_helper.h"
#include "extensions/browser/api/web_request/web_request_info.h"
#include "extensions/browser/api/web_request/web_request_permissions.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_util.h"
#include "extensions/browser/install_prefs_helper.h"
#include "extensions/common/api/declarative_net_request.h"
#include "extensions/common/constants.h"
#include "extensions/common/switches.h"
#include "url/origin.h"

namespace extensions::declarative_net_request {
namespace {

namespace flat_rule = url_pattern_index::flat;
namespace dnr_api = api::declarative_net_request;
using PageAccess = PermissionsData::PageAccess;

void NotifyRequestWithheld(const ExtensionId& extension_id,
                           const WebRequestInfo& request) {
  DCHECK(ExtensionsAPIClient::Get());
  ExtensionsAPIClient::Get()->NotifyWebRequestWithheld(
      request.render_process_id, request.frame_routing_id, extension_id);
}

// Helper to log the time taken in RulesetManager::EvaluateRequestInternal.
class ScopedEvaluateRequestTimer {
 public:
  ScopedEvaluateRequestTimer() = default;
  ~ScopedEvaluateRequestTimer() {
    UMA_HISTOGRAM_CUSTOM_MICROSECONDS_TIMES(
        "Extensions.DeclarativeNetRequest.EvaluateRequestTime.AllExtensions3",
        timer_.Elapsed(), base::Microseconds(1), base::Milliseconds(50), 50);
  }

 private:
  base::ElapsedTimer timer_;
};

}  // namespace

RulesetManager::RulesetManager(content::BrowserContext* browser_context)
    : browser_context_(browser_context),
      prefs_(ExtensionPrefs::Get(browser_context)),
      permission_helper_(PermissionHelper::Get(browser_context)) {
  DCHECK(browser_context_);

  // RulesetManager can be created on any sequence.
  DETACH_FROM_SEQUENCE(sequence_checker_);
}

RulesetManager::~RulesetManager() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}

void RulesetManager::AddRuleset(const ExtensionId& extension_id,
                                std::unique_ptr<CompositeMatcher> matcher) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  // TODO(crbug.com/358617943): Replace LOG_IF() and histogram with CHECK().
  const bool called_twice = GetMatcherForExtension(extension_id);
  LOG_IF(ERROR, called_twice)
      << "AddRuleset called twice in succession for " << extension_id;
  base::UmaHistogramBoolean(
      "Extensions.DeclarativeNetRequest.AddRulesetCalledTwiceInSuccession",
      called_twice);

  base::Time update_time = GetLastUpdateTime(prefs_, extension_id);
  rulesets_.emplace(extension_id, update_time, std::move(matcher));
  extension_install_times_[extension_id] = update_time;

  if (test_observer_) {
    test_observer_->OnRulesetCountChanged(rulesets_.size());
  }

  // Clear the renderers' cache so that they take the new rules into account.
  ClearRendererCacheOnNavigation();
}

void RulesetManager::RemoveRuleset(const ExtensionId& extension_id) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  auto compare_by_id =
      [&extension_id](const ExtensionRulesetData& ruleset_data) {
        return ruleset_data.extension_id == extension_id;
      };

  extension_install_times_.erase(extension_id);
  size_t erased_count = base::EraseIf(rulesets_, compare_by_id);
  DCHECK_EQ(1u, erased_count)
      << "RemoveRuleset called without a corresponding AddRuleset for "
      << extension_id;

  if (test_observer_) {
    test_observer_->OnRulesetCountChanged(rulesets_.size());
  }

  // Clear the renderers' cache so that they take the removed rules into
  // account.
  ClearRendererCacheOnNavigation();
}

std::set<ExtensionId> RulesetManager::GetExtensionsWithRulesets() const {
  std::set<ExtensionId> extension_ids;
  for (const ExtensionRulesetData& data : rulesets_) {
    extension_ids.insert(data.extension_id);
  }
  return extension_ids;
}

CompositeMatcher* RulesetManager::GetMatcherForExtension(
    const ExtensionId& extension_id) {
  return const_cast<CompositeMatcher*>(
      static_cast<const RulesetManager*>(this)->GetMatcherForExtension(
          extension_id));
}

const CompositeMatcher* RulesetManager::GetMatcherForExtension(
    const ExtensionId& extension_id) const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // This is O(n) but it's ok since the number of extensions will be small and
  // we have to maintain the rulesets sorted in decreasing order of installation
  // time.
  auto iter = std::ranges::find(rulesets_, extension_id,
                                &ExtensionRulesetData::extension_id);

  // There must be ExtensionRulesetData corresponding to this |extension_id|.
  if (iter == rulesets_.end()) {
    return nullptr;
  }

  DCHECK(iter->matcher);
  return iter->matcher.get();
}

const std::vector<RequestAction>& RulesetManager::EvaluateBeforeRequest(
    const WebRequestInfo& request,
    bool is_incognito_context) const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // Note: it's safe to cache the action on WebRequestInfo without worrying
  // about |is_incognito_context| since a WebRequestInfo object will not be
  // shared between different contexts. Hence the value of
  // |is_incognito_context| will stay the same for a given |request|. This also
  // assumes that the core state of the WebRequestInfo isn't changed between the
  // different EvaluateRequest invocations.
  // Note: Since this is called before the request is sent, `response_headers`
  // have not been received yet and is null.
  if (!request.dnr_actions) {
    request.dnr_actions = EvaluateRequestInternal(
        request, /*response_headers=*/nullptr, is_incognito_context);
  }

  return *request.dnr_actions;
}

std::vector<RequestAction> RulesetManager::EvaluateRequestWithHeaders(
    const WebRequestInfo& request,
    const net::HttpResponseHeaders* response_headers,
    bool is_incognito_context) const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (!response_headers) {
    // NOTE: This can happen for auth challenges from CORS preflight requests
    // that get routed to the respective main request's handler (since they
    // share the same `request_id`), which happens when `extraHeaders` is
    // enabled.
    //
    // At this point, the main request is paused waiting for the preflight, and
    // has not received its own headers, so `response_headers` is null. We must
    // return early. See https://crbug.com/444248440.
    return {};
  }

  return EvaluateRequestInternal(request, response_headers,
                                 is_incognito_context);
}

bool RulesetManager::HasAnyExtraHeadersMatcher() const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  return std::ranges::any_of(
      rulesets_, [](const ExtensionRulesetData& ruleset) {
        return ruleset.matcher->HasAnyExtraHeadersMatcher();
      });
}

bool RulesetManager::HasExtraHeadersMatcherForRequest(
    const WebRequestInfo& request,
    bool is_incognito_context) const {
  const std::vector<RequestAction>& actions =
      EvaluateBeforeRequest(request, is_incognito_context);

  static_assert(flat::ActionType_count == 6,
                "Modify this method to ensure HasExtraHeadersMatcherForRequest "
                "is updated as new actions are added.");

  // TODO(kelvinjiang): We can optimize this check for the onHeadersReceived
  // stage by looking for particular headers required for extraHeaders or if the
  // request would potentially match any onHeadersReceived rules based on
  // non-header parameters.
  return HasRulesets(RulesetMatchingStage::kOnHeadersReceived) ||
         base::Contains(actions, RequestAction::Type::MODIFY_HEADERS,
                        &RequestAction::type);
}

void RulesetManager::OnRenderFrameCreated(content::RenderFrameHost* host) {
  for (ExtensionRulesetData& ruleset : rulesets_) {
    ruleset.matcher->OnRenderFrameCreated(host);
  }
}

void RulesetManager::OnRenderFrameDeleted(content::RenderFrameHost* host) {
  for (ExtensionRulesetData& ruleset : rulesets_) {
    ruleset.matcher->OnRenderFrameDeleted(host);
  }
}

void RulesetManager::OnDidFinishNavigation(
    content::NavigationHandle* navigation_handle) {
  for (ExtensionRulesetData& ruleset : rulesets_) {
    ruleset.matcher->OnDidFinishNavigation(navigation_handle);
  }
}

bool RulesetManager::HasRulesets(RulesetMatchingStage stage) const {
  return std::ranges::any_of(rulesets_,
                             [stage](const ExtensionRulesetData& ruleset) {
                               return ruleset.matcher->HasRulesets(stage);
                             });
}

std::vector<RequestAction> RulesetManager::MergeModifyHeaderActions(
    std::vector<RequestAction> lhs_actions,
    std::vector<RequestAction> rhs_actions) const {
  std::vector<RequestAction> merged_actions;
  merged_actions.reserve(lhs_actions.size() + rhs_actions.size());

  merged_actions.insert(merged_actions.end(),
                        std::make_move_iterator(lhs_actions.begin()),
                        std::make_move_iterator(lhs_actions.end()));
  merged_actions.insert(merged_actions.end(),
                        std::make_move_iterator(rhs_actions.begin()),
                        std::make_move_iterator(rhs_actions.end()));

  std::sort(
      merged_actions.begin(), merged_actions.end(),
      [this](const RequestAction& lhs, const RequestAction& rhs) {
        auto lhs_install_time_it =
            extension_install_times_.find(lhs.extension_id);
        CHECK(lhs_install_time_it != extension_install_times_.end());

        auto rhs_install_time_it =
            extension_install_times_.find(rhs.extension_id);
        CHECK(rhs_install_time_it != extension_install_times_.end());

        // Same comparator as ExtensionRulesetData's for actions from different
        // extensions. Otherwise, default to RequestAction's comparator.
        return std::tie(lhs_install_time_it->second, lhs.extension_id, lhs) >
               std::tie(rhs_install_time_it->second, rhs.extension_id, rhs);
      });
  return merged_actions;
}

void RulesetManager::SetObserverForTest(TestObserver* observer) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(!(test_observer_ && observer))
      << "Multiple test observers are not supported";
  test_observer_ = observer;
}

RulesetManager::ExtensionRulesetData::ExtensionRulesetData(
    const ExtensionId& extension_id,
    const base::Time& extension_install_time,
    std::unique_ptr<CompositeMatcher> matcher)
    : extension_id(extension_id),
      extension_install_time(extension_install_time),
      matcher(std::move(matcher)) {}
RulesetManager::ExtensionRulesetData::~ExtensionRulesetData() = default;
RulesetManager::ExtensionRulesetData::ExtensionRulesetData(
    ExtensionRulesetData&& other) = default;
RulesetManager::ExtensionRulesetData& RulesetManager::ExtensionRulesetData::
operator=(ExtensionRulesetData&& other) = default;

bool RulesetManager::ExtensionRulesetData::operator<(
    const ExtensionRulesetData& other) const {
  // Sort based on *descending* installation time, using extension id to break
  // ties.
  return std::tie(extension_install_time, extension_id) >
         std::tie(other.extension_install_time, other.extension_id);
}

std::optional<RequestAction> RulesetManager::GetAction(
    const std::vector<RulesetAndPageAccess>& rulesets,
    const WebRequestInfo& request,
    const RequestParams& params,
    RulesetMatchingStage stage) const {
  DCHECK(std::is_sorted(rulesets.begin(), rulesets.end(),
                        [](RulesetAndPageAccess a, RulesetAndPageAccess b) {
                          return *a.first < *b.first;
                        }));

  // The priorities of actions between different extensions is different from
  // the priorities of actions within an extension.
  const auto action_priority = [](const std::optional<RequestAction>& action) {
    if (!action.has_value()) {
      return 0;
    }
    switch (action->type) {
      case RequestAction::Type::BLOCK:
      case RequestAction::Type::COLLAPSE:
        return 3;
      case RequestAction::Type::REDIRECT:
      case RequestAction::Type::UPGRADE:
        return 2;
      case RequestAction::Type::ALLOW:
      case RequestAction::Type::ALLOW_ALL_REQUESTS:
        return 1;
      case RequestAction::Type::MODIFY_HEADERS:
        NOTREACHED();
    }
  };

  std::optional<RequestAction> action;

  // This iterates in decreasing order of extension installation time. Hence
  // more recently installed extensions get higher priority in choosing the
  // action for the request.
  for (const RulesetAndPageAccess& ruleset_and_access : rulesets) {
    const ExtensionRulesetData* ruleset = ruleset_and_access.first;

    CompositeMatcher::ActionInfo action_info =
        ruleset->matcher->GetAction(params, stage, ruleset_and_access.second);

    DCHECK(!(action_info.action && action_info.notify_request_withheld));
    if (action_info.notify_request_withheld) {
      NotifyRequestWithheld(ruleset->extension_id, request);
      continue;
    }

    // If there is a tie here, `action` (from the more recently installed
    // extension) wins.
    if (action_priority(action_info.action) > action_priority(action)) {
      action = std::move(action_info.action);
    }
  }

  return action;
}

std::vector<RequestAction> RulesetManager::GetModifyHeadersActions(
    const std::vector<RulesetAndPageAccess>& rulesets,
    const WebRequestInfo& request,
    const RequestParams& params,
    RulesetMatchingStage stage) const {
  DCHECK(std::is_sorted(rulesets.begin(), rulesets.end(),
                        [](RulesetAndPageAccess a, RulesetAndPageAccess b) {
                          return *a.first < *b.first;
                        }));

  std::vector<RequestAction> modify_headers_actions;

  for (const RulesetAndPageAccess& ruleset_and_access : rulesets) {
    PageAccess page_access = ruleset_and_access.second;
    // Skip the evaluation of modifyHeaders rules for this extension if its
    // access to the request is denied.
    if (page_access == PageAccess::kDenied) {
      continue;
    }

    const ExtensionRulesetData* ruleset = ruleset_and_access.first;
    std::vector<RequestAction> actions_for_matcher =
        ruleset->matcher->GetModifyHeadersActions(params, stage);

    // Evaluate modifyHeaders rules for this extension if and only if it has
    // host permissions for the request url and initiator.
    if (page_access == PageAccess::kAllowed) {
      modify_headers_actions.insert(
          modify_headers_actions.end(),
          std::make_move_iterator(actions_for_matcher.begin()),
          std::make_move_iterator(actions_for_matcher.end()));
    } else if (page_access == PageAccess::kWithheld &&
               !actions_for_matcher.empty()) {
      // Notify the extension that it could not modify the request's headers if
      // it had at least one matching modifyHeaders rule and its access to the
      // request was withheld.
      NotifyRequestWithheld(ruleset->extension_id, request);
    }
  }

  // `modify_headers_actions` is implicitly sorted in decreasing order by
  // priority.
  //  - Within an extension: each CompositeMatcher returns a vector sorted by
  //  priority.
  //  - Between extensions: |rulesets| is sorder in descending order of
  //  extension priority.
  return modify_headers_actions;
}

std::vector<RequestAction> RulesetManager::EvaluateRequestInternal(
    const WebRequestInfo& request,
    const net::HttpResponseHeaders* response_headers,
    bool is_incognito_context) const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  RulesetMatchingStage stage = response_headers
                                   ? RulesetMatchingStage::kOnHeadersReceived
                                   : RulesetMatchingStage::kOnBeforeRequest;
  if (!response_headers) {
    DCHECK(!request.dnr_actions);
  }

  std::vector<RequestAction> actions;

  if (!ShouldEvaluateRequest(request)) {
    return actions;
  }

  // TODO(crbug.com/40727004): Add some context on which request stage this
  // event took place in the observer method if/when needed for tests.
  if (test_observer_) {
    test_observer_->OnEvaluateRequest(request, is_incognito_context);
  }

  if (rulesets_.empty()) {
    return actions;
  }

  ScopedEvaluateRequestTimer timer;

  // Filter the rulesets to evaluate along with their host permissions based
  // page access for the current request being evaluated.
  std::vector<RulesetAndPageAccess> rulesets_to_evaluate;
  for (const ExtensionRulesetData& ruleset : rulesets_) {
    PageAccess host_permission_access = PageAccess::kDenied;
    if (!ShouldEvaluateRulesetForRequest(ruleset, request, is_incognito_context,
                                         host_permission_access)) {
      continue;
    }

    rulesets_to_evaluate.emplace_back(&ruleset, host_permission_access);
  }

  // Check that the allow rule priority cache from `request` is empty if the
  // request has not been evaluated yet in the kOnBeforeRequest stage.
  CHECK(stage != RulesetMatchingStage::kOnBeforeRequest ||
        request.max_priority_allow_action.empty());

  const RequestParams params(request, response_headers);
  std::optional<RequestAction> action =
      GetAction(rulesets_to_evaluate, request, params, stage);

  if (action) {
    bool is_request_modifying_action = !action->IsAllowOrAllowAllRequests();
    actions.push_back(std::move(*action));

    // If the request is blocked/redirected, no further modifications can
    // happen.
    if (is_request_modifying_action) {
      return actions;
    }
  }

  // This returns any matching modifyHeaders rules with priority greater than
  // matching allow/allowAllRequests rules.
  std::vector<RequestAction> modify_headers_actions =
      GetModifyHeadersActions(rulesets_to_evaluate, request, params, stage);

  // Pass the allow rule priority cache to `request` so its current value can be
  // reused in later rule matching stages.
  request.max_priority_allow_action =
      std::move(params.max_priority_allow_action);

  if (!modify_headers_actions.empty()) {
    return modify_headers_actions;
  }

  return actions;
}

bool RulesetManager::ShouldEvaluateRequest(
    const WebRequestInfo& request) const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // Ensure clients filter out sensitive requests.
  DCHECK(!WebRequestPermissions::HideRequest(permission_helper_, request));

  // Prevent extensions from modifying any resources on the chrome-extension
  // scheme. Practically, this has the effect of not allowing an extension to
  // modify its own resources (The extension wouldn't have the permission to
  // other extension origins anyway).
#if BUILDFLAG(ARKWEB_ARKWEB_EXTENSIONS)
  if (request.url.SchemeIs(kArkwebExtensionScheme)) {
    return false;
  }
#endif
  if (request.url.SchemeIs(kExtensionScheme)) {
    return false;
  }

  // Declarative Net Request rules should not be matched against requests
  // originating from WebViews.
  if (request.is_web_view) {
    return false;
  }

  // Declarative Net Request rules should not be matched against requests
  // originating from WebViews.
  if (request.is_web_view) {
    return false;
  }

  return true;
}

bool RulesetManager::ShouldEvaluateRulesetForRequest(
    const ExtensionRulesetData& ruleset,
    const WebRequestInfo& request,
    bool is_incognito_context,
    PageAccess& host_permission_access) const {
  // Extensions should not generally have access to non-main-frame requests
  // initiated by other extensions, though the --extensions-on-chrome-urls
  // switch overrides that restriction.
  // Note: For discussions regarding handling of extension initiated navigations
  //       see https://crbug.com/918137 and https://crbug.com/382670035.
  if (!switches::AreExtensionsOnExtensionURLsAllowed() && request.initiator &&
      request.web_request_type != WebRequestResourceType::MAIN_FRAME) {
    // Checking the precursor is necessary here since requests initiated by
    // manifest sandbox pages have an opaque initiator origin, but still
    // originate from an extension.
    auto initiator_precursor =
        request.initiator->GetTupleOrPrecursorTupleIfOpaque();
    if ((initiator_precursor.scheme() == kExtensionScheme
#if BUILDFLAG(ARKWEB_ARKWEB_EXTENSIONS)
         || initiator_precursor.scheme() == extensions::kArkwebExtensionScheme
#endif
         ) &&
        initiator_precursor.host() != ruleset.extension_id) {
      return false;
    }
  }

  // Only extensions enabled in incognito should have access to requests in an
  // incognito context.
  if (is_incognito_context &&
      !util::IsIncognitoEnabled(ruleset.extension_id, browser_context_)) {
    return false;
  }

  const int tab_id = request.frame_data.tab_id;

  // `crosses_incognito` is used to ensure that a split mode extension process
  // can't intercept requests from a cross browser context. Since declarative
  // net request API doesn't use event listeners in a background process, it
  // is irrelevant here.
  const bool crosses_incognito = false;

  switch (ruleset.matcher->host_permissions_always_required()) {
    case HostPermissionsAlwaysRequired::kTrue: {
      PageAccess access = WebRequestPermissions::CanExtensionAccessURL(
          permission_helper_, ruleset.extension_id, request.url, tab_id,
          crosses_incognito,
          WebRequestPermissions::REQUIRE_HOST_PERMISSION_FOR_URL_AND_INITIATOR,
          request.initiator, request.web_request_type);
      if (access == PageAccess::kDenied) {
        return false;
      }

      host_permission_access = access;
      break;
    }

    case HostPermissionsAlwaysRequired::kFalse: {
      // Some requests should not be visible to extensions even if the extension
      // doesn't require host permissions for them. Note: we are not checking
      // for host permissions here.
      // DO_NOT_CHECK_HOST is strictly less restrictive than
      // REQUIRE_HOST_PERMISSION_FOR_URL_AND_INITIATOR.
      PageAccess do_not_check_host_access =
          WebRequestPermissions::CanExtensionAccessURL(
              permission_helper_, ruleset.extension_id, request.url, tab_id,
              crosses_incognito, WebRequestPermissions::DO_NOT_CHECK_HOST,
              request.initiator, request.web_request_type);
      DCHECK_NE(PageAccess::kWithheld, do_not_check_host_access);
      if (do_not_check_host_access == PageAccess::kDenied) {
        return false;
      }

      host_permission_access = WebRequestPermissions::CanExtensionAccessURL(
          permission_helper_, ruleset.extension_id, request.url, tab_id,
          crosses_incognito,
          WebRequestPermissions::REQUIRE_HOST_PERMISSION_FOR_URL_AND_INITIATOR,
          request.initiator, request.web_request_type);
      break;
    }
  }

  return true;
}

}  // namespace extensions::declarative_net_request