// 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/common/url_pattern_set.h"

#include <iterator>
#include <ostream>

#include "base/logging.h"
#include "base/no_destructor.h"
#include "base/stl_util.h"
#include "extensions/common/error_utils.h"
#include "extensions/common/url_pattern.h"
#include "url/gurl.h"
#include "url/origin.h"
#include "url/url_constants.h"

namespace extensions {

namespace {

const char kInvalidURLPatternError[] = "Invalid url pattern '*'";

}  // namespace

// static
const URLPatternSet& URLPatternSet::Empty() {
  static base::NoDestructor<URLPatternSet> empty_url_pattern;
  return *empty_url_pattern.get();
}

// static
URLPatternSet URLPatternSet::CreateDifference(const URLPatternSet& set1,
                                              const URLPatternSet& set2) {
  return URLPatternSet(base::STLSetDifference<std::set<URLPattern>>(
      set1.patterns_, set2.patterns_));
}

// static
URLPatternSet URLPatternSet::CreateIntersection(
    const URLPatternSet& set1,
    const URLPatternSet& set2,
    IntersectionBehavior intersection_behavior) {
  // Note: leverage return value optimization; always return the same object.
  URLPatternSet result;

  if (intersection_behavior == IntersectionBehavior::kStringComparison) {
    // String comparison just relies on STL set behavior, which looks at the
    // string representation.
    result = URLPatternSet(base::STLSetIntersection<std::set<URLPattern>>(
        set1.patterns_, set2.patterns_));
    return result;
  }

  // Look for a semantic intersection.

  // Step 1: Iterate over each set. Find any patterns that are completely
  // contained by the other (thus being necessarily present in any intersection)
  // and add them, collecting the others in a set of unique items.
  // Note: Use a collection of pointers for the uniques to avoid excessive
  // copies. Since these are owned by the URLPatternSet passed in, which is
  // const, this should be safe.
  std::vector<const URLPattern*> unique_set1;
  for (const URLPattern& pattern : set1) {
    if (set2.ContainsPattern(pattern)) {
      result.patterns_.insert(pattern);
    } else {
      unique_set1.push_back(&pattern);
    }
  }
  std::vector<const URLPattern*> unique_set2;
  for (const URLPattern& pattern : set2) {
    if (set1.ContainsPattern(pattern)) {
      result.patterns_.insert(pattern);
    } else {
      unique_set2.push_back(&pattern);
    }
  }

  // If we're just looking for patterns contained by both, we're done.
  if (intersection_behavior == IntersectionBehavior::kPatternsContainedByBoth) {
    return result;
  }

  DCHECK_EQ(IntersectionBehavior::kDetailed, intersection_behavior);

  // Step 2: Iterate over all the unique patterns and find the intersections
  // they have with the other patterns.
  for (const auto* pattern : unique_set1) {
    for (const auto* pattern2 : unique_set2) {
      std::optional<URLPattern> intersection =
          pattern->CreateIntersection(*pattern2);
      if (intersection) {
        result.patterns_.insert(std::move(*intersection));
      }
    }
  }

  return result;
}

// static
URLPatternSet URLPatternSet::CreateUnion(const URLPatternSet& set1,
                                         const URLPatternSet& set2) {
  return URLPatternSet(
      base::STLSetUnion<std::set<URLPattern>>(set1.patterns_, set2.patterns_));
}

URLPatternSet::URLPatternSet() = default;

URLPatternSet::URLPatternSet(URLPatternSet&& rhs) = default;

URLPatternSet::URLPatternSet(const std::set<URLPattern>& patterns)
    : patterns_(patterns) {}

URLPatternSet::~URLPatternSet() = default;

URLPatternSet& URLPatternSet::operator=(URLPatternSet&& rhs) = default;

bool URLPatternSet::operator==(const URLPatternSet& other) const {
  return patterns_ == other.patterns_;
}

std::ostream& operator<<(std::ostream& out,
                         const URLPatternSet& url_pattern_set) {
  out << "{ ";

  auto iter = url_pattern_set.patterns().cbegin();
  if (!url_pattern_set.patterns().empty()) {
    out << *iter;
    ++iter;
  }

  for (; iter != url_pattern_set.patterns().end(); ++iter) {
    out << ", " << *iter;
  }

  if (!url_pattern_set.patterns().empty()) {
    out << " ";
  }

  out << "}";
  return out;
}

URLPatternSet URLPatternSet::Clone() const {
  return URLPatternSet(patterns_);
}

bool URLPatternSet::is_empty() const {
  return patterns_.empty();
}

size_t URLPatternSet::size() const {
  return patterns_.size();
}

bool URLPatternSet::AddPattern(const URLPattern& pattern) {
  return patterns_.insert(pattern).second;
}

void URLPatternSet::AddPatterns(const URLPatternSet& set) {
  patterns_.insert(set.patterns().begin(),
                   set.patterns().end());
}

void URLPatternSet::ClearPatterns() {
  patterns_.clear();
}

bool URLPatternSet::AddOrigin(int valid_schemes, const GURL& origin) {
  if (origin.is_empty()) {
    return false;
  }
  const url::Origin real_origin = url::Origin::Create(origin);
  DCHECK(real_origin.IsSameOriginWith(
      url::Origin::Create(origin.DeprecatedGetOriginAsURL())));
  // TODO(devlin): Implement this in terms of the `AddOrigin()` call that takes
  // an url::Origin? It's interesting because this doesn't currently supply an
  // extra path, so if the GURL has not path ("https://example.com"), it would
  // fail to add - which is probably a bug.
  URLPattern origin_pattern(valid_schemes);
  // Origin adding could fail if |origin| does not match |valid_schemes|.
  if (origin_pattern.Parse(origin.spec()) !=
      URLPattern::ParseResult::kSuccess) {
    return false;
  }
  origin_pattern.SetPath("/*");
  return AddPattern(origin_pattern);
}

bool URLPatternSet::AddOrigin(int valid_schemes, const url::Origin& origin) {
  DCHECK(!origin.opaque());
  URLPattern origin_pattern(valid_schemes);
  // Origin adding could fail if |origin| does not match |valid_schemes|.
  std::string string_pattern = origin.Serialize() + "/*";
  if (origin_pattern.Parse(string_pattern) !=
      URLPattern::ParseResult::kSuccess) {
    return false;
  }
  return AddPattern(origin_pattern);
}

bool URLPatternSet::Contains(const URLPatternSet& other) const {
  for (const auto& it : other) {
    if (!ContainsPattern(it)) {
      return false;
    }
  }

  return true;
}

bool URLPatternSet::ContainsPattern(const URLPattern& pattern) const {
  for (const auto& it : *this) {
    if (it.Contains(pattern)) {
      return true;
    }
  }
  return false;
}

bool URLPatternSet::MatchesURL(const GURL& url) const {
  return MatchesURL(url, /*case_sensitive=*/true);
}

bool URLPatternSet::MatchesURL(const GURL& url, bool case_sensitive) const {
  for (const auto& pattern : patterns_) {
    if (pattern.MatchesURL(url, case_sensitive)) {
      return true;
    }
  }

  return false;
}

bool URLPatternSet::MatchesAllURLs() const {
  for (const auto& host : *this) {
    if (host.match_all_urls() ||
        (host.match_subdomains() && host.host().empty())) {
      return true;
    }
  }
  return false;
}

bool URLPatternSet::MatchesHost(const GURL& test,
                                bool require_match_subdomains) const {
  if (!test.is_valid()) {
    return false;
  }

  return std::any_of(
      patterns_.begin(), patterns_.end(),
      [&test, require_match_subdomains](const URLPattern& pattern) {
        return pattern.MatchesHost(test) &&
               (!require_match_subdomains || pattern.match_subdomains());
      });
}

bool URLPatternSet::MatchesSecurityOrigin(const GURL& origin) const {
  for (const auto& pattern : patterns_) {
    if (pattern.MatchesSecurityOrigin(origin)) {
      return true;
    }
  }

  return false;
}

bool URLPatternSet::OverlapsWith(const URLPatternSet& other) const {
  // Two extension extents overlap if there is any one URL that would match at
  // least one pattern in each of the extents.
  for (const auto& pattern : patterns_) {
    for (const auto& j : other.patterns()) {
      if (pattern.OverlapsWith(j)) {
        return true;
      }
    }
  }

  return false;
}

base::ListValue URLPatternSet::ToValue() const {
  base::ListValue result;
  for (const auto& pattern : patterns_) {
    std::string pattern_str_value(pattern.GetAsString());
    if (!result.contains(pattern_str_value)) {
      result.Append(pattern_str_value);
    }
  }
  return result;
}

bool URLPatternSet::Populate(const std::vector<std::string>& patterns,
                             int valid_schemes,
                             bool allow_file_access,
                             std::string* error) {
  ClearPatterns();
  for (const auto& i : patterns) {
    URLPattern pattern(valid_schemes);
    if (pattern.Parse(i) != URLPattern::ParseResult::kSuccess) {
      if (error) {
        *error = ErrorUtils::FormatErrorMessage(kInvalidURLPatternError, i);
      } else {
        LOG(ERROR) << "Invalid url pattern: " << i;
      }
      return false;
    }
    if (!allow_file_access && pattern.MatchesScheme(url::kFileScheme)) {
      pattern.SetValidSchemes(
          pattern.valid_schemes() & ~URLPattern::SCHEME_FILE);
    }
    AddPattern(pattern);
  }
  return true;
}

std::vector<std::string> URLPatternSet::ToStringVector() const {
  std::vector<std::string> result;
  for (const auto& pattern : patterns_) {
    result.push_back(pattern.GetAsString());
  }
  return result;
}

bool URLPatternSet::Populate(const base::ListValue& value,
                             int valid_schemes,
                             bool allow_file_access,
                             std::string* error) {
  std::vector<std::string> patterns;
  for (const base::Value& pattern : value) {
    const std::string* item = pattern.GetIfString();
    if (!item) {
      return false;
    }
    patterns.push_back(*item);
  }
  return Populate(patterns, valid_schemes, allow_file_access, error);
}

}  // namespace extensions
