// Copyright 2019 The Chromium Authors. All rights reserved.
// 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/composite_matcher.h"

#include <algorithm>
#include <set>
#include <utility>

#include "base/metrics/histogram_macros.h"
#include "base/time/time.h"
#include "base/timer/elapsed_timer.h"
#include "extensions/browser/api/declarative_net_request/flat/extension_ruleset_generated.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"

namespace extensions {
namespace declarative_net_request {
namespace flat_rule = url_pattern_index::flat;
using PageAccess = PermissionsData::PageAccess;
using ActionInfo = CompositeMatcher::ActionInfo;

namespace {

bool AreIDsUnique(const CompositeMatcher::MatcherList& matchers) {
  std::set<size_t> ids;
  for (const auto& matcher : matchers) {
    bool did_insert = ids.insert(matcher->id()).second;
    if (!did_insert)
      return false;
  }

  return true;
}

// Helper to log the time taken in CompositeMatcher::GetBeforeRequestAction.
class ScopedGetBeforeRequestActionTimer {
 public:
  ScopedGetBeforeRequestActionTimer() = default;
  ~ScopedGetBeforeRequestActionTimer() {
    UMA_HISTOGRAM_CUSTOM_MICROSECONDS_TIMES(
        "Extensions.DeclarativeNetRequest.EvaluateBeforeRequestTime."
        "SingleExtension2",
        timer_.Elapsed(), base::TimeDelta::FromMicroseconds(1),
        base::TimeDelta::FromMilliseconds(50), 50);
  }

 private:
  base::ElapsedTimer timer_;
};

}  // namespace

ActionInfo::ActionInfo(base::Optional<RequestAction> action,
                       bool notify_request_withheld)
    : action(std::move(action)),
      notify_request_withheld(notify_request_withheld) {}

ActionInfo::~ActionInfo() = default;

ActionInfo::ActionInfo(ActionInfo&&) = default;
ActionInfo& ActionInfo::operator=(ActionInfo&& other) = default;

CompositeMatcher::CompositeMatcher(MatcherList matchers)
    : matchers_(std::move(matchers)) {
  DCHECK(AreIDsUnique(matchers_));
}

CompositeMatcher::~CompositeMatcher() = default;

void CompositeMatcher::AddOrUpdateRuleset(
    std::unique_ptr<RulesetMatcher> new_matcher) {
  // A linear search is ok since the number of rulesets per extension is
  // expected to be quite small.
  auto it = std::find_if(
      matchers_.begin(), matchers_.end(),
      [&new_matcher](const std::unique_ptr<RulesetMatcher>& matcher) {
        return new_matcher->id() == matcher->id();
      });

  if (it == matchers_.end()) {
    matchers_.push_back(std::move(new_matcher));
  } else {
    // Update the matcher.
    *it = std::move(new_matcher);
  }

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

ActionInfo CompositeMatcher::GetBeforeRequestAction(
    const RequestParams& params,
    PageAccess page_access) const {
  ScopedGetBeforeRequestActionTimer timer;

  bool notify_request_withheld = false;
  base::Optional<RequestAction> final_action;
  for (const auto& matcher : matchers_) {
    base::Optional<RequestAction> action =
        matcher->GetBeforeRequestAction(params);
    params.allow_rule_cache[matcher.get()] =
        action && action->IsAllowOrAllowAllRequests();

    if (action && action->type == RequestAction::Type::REDIRECT) {
      // Redirecting requires host permissions.
      // TODO(crbug.com/1033780): returning base::nullopt here results in
      // counterintuitive behavior.
      if (page_access == PageAccess::kDenied) {
        action = base::nullopt;
      } else if (page_access == PageAccess::kWithheld) {
        action = base::nullopt;
        notify_request_withheld = true;
      }
    }

    final_action =
        GetMaxPriorityAction(std::move(final_action), std::move(action));
  }

  if (final_action)
    return ActionInfo(std::move(final_action), false);
  return ActionInfo(base::nullopt, notify_request_withheld);
}

uint8_t CompositeMatcher::GetRemoveHeadersMask(
    const RequestParams& params,
    uint8_t excluded_remove_headers_mask,
    std::vector<RequestAction>* remove_headers_actions) const {
  uint8_t mask = 0;
  for (const auto& matcher : matchers_) {
    // An allow rule will override lower priority remove header rules.
    if (!params.allow_rule_cache.contains(matcher.get())) {
      // GetBeforeRequestAction is normally called before GetRemoveHeadersMask,
      // so this should never happen in non-test builds. There are tests that
      // call GetRemoveHeadersMask directly, though.
      base::Optional<RequestAction> action =
          matcher->GetBeforeRequestAction(params);
      params.allow_rule_cache[matcher.get()] =
          action && action->IsAllowOrAllowAllRequests();
    }
    if (params.allow_rule_cache[matcher.get()])
      return mask;

    mask |= matcher->GetRemoveHeadersMask(
        params, mask | excluded_remove_headers_mask, remove_headers_actions);
  }

  DCHECK(!(mask & excluded_remove_headers_mask));
  return mask;
}

bool CompositeMatcher::HasAnyExtraHeadersMatcher() const {
  if (!has_any_extra_headers_matcher_.has_value())
    has_any_extra_headers_matcher_ = ComputeHasAnyExtraHeadersMatcher();
  return has_any_extra_headers_matcher_.value();
}

void CompositeMatcher::OnRenderFrameCreated(content::RenderFrameHost* host) {
  for (auto& matcher : matchers_)
    matcher->OnRenderFrameCreated(host);
}

void CompositeMatcher::OnRenderFrameDeleted(content::RenderFrameHost* host) {
  for (auto& matcher : matchers_)
    matcher->OnRenderFrameDeleted(host);
}

void CompositeMatcher::OnDidFinishNavigation(content::RenderFrameHost* host) {
  for (auto& matcher : matchers_)
    matcher->OnDidFinishNavigation(host);
}

bool CompositeMatcher::ComputeHasAnyExtraHeadersMatcher() const {
  for (const auto& matcher : matchers_) {
    if (matcher->IsExtraHeadersMatcher())
      return true;
  }
  return false;
}

}  // namespace declarative_net_request
}  // namespace extensions
