// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/lookalikes/core/safety_tips_config.h"

#include <algorithm>

#include "base/no_destructor.h"
#include "components/safe_browsing/core/browser/db/v4_protocol_manager_util.h"
#include "third_party/re2/src/re2/re2.h"
#include "url/gurl.h"

using safe_browsing::SBProtocolManagerUtil;

namespace lookalikes {

namespace {

class SafetyTipsConfigSingleton {
 public:
  void SetProto(std::unique_ptr<reputation::SafetyTipsConfig> proto) {
    proto_ = std::move(proto);
  }

  reputation::SafetyTipsConfig* GetProto() const { return proto_.get(); }

  static SafetyTipsConfigSingleton& GetInstance() {
    static base::NoDestructor<SafetyTipsConfigSingleton> instance;
    return *instance;
  }

 private:
  std::unique_ptr<reputation::SafetyTipsConfig> proto_;
};

// Given a URL, generates all possible variant URLs to check the blocklist for.
// This is conceptually almost identical to safe_browsing::UrlToFullHashes, but
// without the hashing step.
//
// Note: Blocking "a.b/c/" does NOT block http://a.b/c without the trailing /.
void UrlToSafetyTipPatterns(const GURL& url,
                            std::vector<std::string>* patterns) {
  std::string canon_host;
  std::string canon_path;
  std::string canon_query;
  SBProtocolManagerUtil::CanonicalizeUrl(url, &canon_host, &canon_path,
                                         &canon_query);

  std::vector<std::string> hosts;
  if (url.HostIsIPAddress()) {
    hosts.push_back(url.GetHost());
  } else {
    SBProtocolManagerUtil::GenerateHostVariantsToCheck(canon_host, &hosts);
  }

  std::vector<std::string> paths;
  SBProtocolManagerUtil::GeneratePathVariantsToCheck(canon_path, canon_query,
                                                     &paths);

  for (const std::string& host : hosts) {
    for (const std::string& path : paths) {
      DCHECK(path.length() == 0 || path[0] == '/');
      patterns->push_back(host + path);
    }
  }
}

// Return whether |canonical_url| is a member of the designated cohort.
bool IsUrlAllowedByCohort(const reputation::SafetyTipsConfig* proto,
                          const GURL& canonical_url,
                          unsigned cohort_index) {
  DCHECK(proto);
  DCHECK(canonical_url.is_valid());

  // Ensure that the cohort index is valid before using it. If it isn't valid,
  // we just pretend the cohort didn't include the canonical URL.
  if (cohort_index >= static_cast<unsigned>(proto->cohort_size())) {
    return false;
  }

  const auto& cohort = proto->cohort(cohort_index);

  // For each possible URL pattern, see if any of the indicated allowed_index or
  // canonical_index entries correspond to a matching pattern since both sets of
  // indices are considered valid spoof targets.
  std::vector<std::string> patterns;
  UrlToSafetyTipPatterns(canonical_url, &patterns);
  for (const auto& search_pattern : patterns) {
    for (const unsigned allowed_index : cohort.allowed_index()) {
      // Skip over invalid indices.
      if (allowed_index >=
          static_cast<unsigned>(proto->allowed_pattern_size())) {
        continue;
      }
      const auto& pattern = proto->allowed_pattern(allowed_index).pattern();
      if (pattern == search_pattern) {
        return true;
      }
    }
    for (const unsigned canonical_index : cohort.canonical_index()) {
      // Skip over invalid indices.
      if (canonical_index >=
          static_cast<unsigned>(proto->canonical_pattern_size())) {
        continue;
      }
      const auto& pattern = proto->canonical_pattern(canonical_index).pattern();
      if (pattern == search_pattern) {
        return true;
      }
    }
  }
  return false;
}

}  // namespace

void SetSafetyTipsRemoteConfigProto(
    std::unique_ptr<reputation::SafetyTipsConfig> proto) {
  SafetyTipsConfigSingleton::GetInstance().SetProto(std::move(proto));
}

const reputation::SafetyTipsConfig* GetSafetyTipsRemoteConfigProto() {
  return SafetyTipsConfigSingleton::GetInstance().GetProto();
}

bool IsUrlAllowlistedBySafetyTipsComponent(
    const reputation::SafetyTipsConfig* proto,
    const GURL& visited_url,
    const GURL& canonical_url) {
  DCHECK(proto);
  DCHECK(visited_url.is_valid());
  std::vector<std::string> patterns;
  UrlToSafetyTipPatterns(visited_url, &patterns);
  auto allowed_patterns = proto->allowed_pattern();
  for (const auto& pattern : patterns) {
    reputation::UrlPattern search_target;
    search_target.set_pattern(pattern);

    auto maybe_before = std::lower_bound(
        allowed_patterns.begin(), allowed_patterns.end(), search_target,
        [](const reputation::UrlPattern& a, const reputation::UrlPattern& b)
            -> bool { return a.pattern() < b.pattern(); });

    if (maybe_before != allowed_patterns.end() &&
        pattern == maybe_before->pattern()) {
      // If no cohorts are given, it's a universal allowlist entry.
      if (maybe_before->cohort_index_size() == 0) {
        return true;
      }

      for (const unsigned cohort_index : maybe_before->cohort_index()) {
        if (IsUrlAllowedByCohort(proto, canonical_url, cohort_index)) {
          return true;
        }
      }
    }
  }
  return false;
}

bool IsTargetHostAllowlistedBySafetyTipsComponent(
    const reputation::SafetyTipsConfig* proto,
    const std::string& hostname) {
  DCHECK(!hostname.empty());
  if (proto == nullptr) {
    return false;
  }
  for (const auto& host_pattern : proto->allowed_target_pattern()) {
    if (!host_pattern.has_regex()) {
      continue;
    }
    DCHECK(!host_pattern.regex().empty());
    const re2::RE2 regex(host_pattern.regex());
    DCHECK(regex.ok());
    if (re2::RE2::FullMatch(hostname, regex)) {
      return true;
    }
  }
  return false;
}

bool IsCommonWordInConfigProto(const reputation::SafetyTipsConfig* proto,
                               const std::string& word) {
  // proto is nullptr when running in non-Lookalike tests.
  if (proto == nullptr) {
    return false;
  }

  auto common_words = proto->common_word();
  DCHECK(std::ranges::is_sorted(common_words.begin(), common_words.end()));
  auto lower = std::lower_bound(
      common_words.begin(), common_words.end(), word,
      [](const std::string& a, const std::string& b) -> bool { return a < b; });

  return lower != common_words.end() && word == *lower;
}

}  // namespace lookalikes
