/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "UrlClassifierExceptionList.h"
#include "nsIEffectiveTLDService.h"
#include "nsIUrlClassifierExceptionListEntry.h"
#include "nsIURI.h"
#include "mozilla/net/UrlClassifierCommon.h"
#include "mozilla/ProfilerMarkers.h"
#include "nsNetCID.h"
#include "nsServiceManagerUtils.h"
#include "mozilla/RustRegex.h"

namespace mozilla::net {

NS_IMPL_ISUPPORTS(UrlClassifierExceptionList, nsIUrlClassifierExceptionList)

NS_IMETHODIMP
UrlClassifierExceptionList::Init(const nsACString& aFeature) {
  mFeature = aFeature;
  return NS_OK;
}

NS_IMETHODIMP
UrlClassifierExceptionList::AddEntry(
    nsIUrlClassifierExceptionListEntry* aEntry) {
  NS_ENSURE_ARG_POINTER(aEntry);

  // From the url patterns in the entry, extract the site and top level site.
  // They are used as keys in the exception entry maps.

  nsAutoCString urlPattern;
  nsresult rv = aEntry->GetUrlPattern(urlPattern);
  NS_ENSURE_SUCCESS(rv, rv);

  nsAutoCString topLevelUrlPattern;
  rv = aEntry->GetTopLevelUrlPattern(topLevelUrlPattern);
  NS_ENSURE_SUCCESS(rv, rv);

  nsAutoCString topLevelSite;
  rv = GetSchemelessSiteFromUrlPattern(topLevelUrlPattern, topLevelSite);
  NS_ENSURE_SUCCESS(rv, rv);

  // topLevelUrlPattern is not mandatory, but if topLevelUrlPattern is set,
  // topLevelSite populated as well.
  NS_ENSURE_TRUE(topLevelUrlPattern.IsEmpty() == topLevelSite.IsEmpty(),
                 NS_ERROR_INVALID_ARG);

  if (MOZ_LOG_TEST(gChannelClassifierLog, LogLevel::Debug)) {
    nsAutoCString entryString;
    (void)aEntry->Describe(entryString);
    UC_LOG_DEBUG(("UrlClassifierExceptionList::%s - Adding entry: %s",
                  __FUNCTION__, entryString.get()));
  }

  // If the top level site is empty, the exception applies across all top
  // level sites. Store it in the global exceptions map.
  if (topLevelSite.IsEmpty()) {
    nsAutoCString site;
    rv = GetSchemelessSiteFromUrlPattern(urlPattern, site);
    NS_ENSURE_SUCCESS(rv, rv);

    // We must be able to parse a site from the url pattern.
    NS_ENSURE_TRUE(!site.IsEmpty(), NS_ERROR_INVALID_ARG);

    mGlobalExceptions.LookupOrInsert(site).AppendElement(aEntry);
    return NS_OK;
  }

  // Otherwise, store it in the site specific exception map.
  mExceptions.LookupOrInsert(topLevelSite).AppendElement(aEntry);

  return NS_OK;
}

NS_IMETHODIMP
UrlClassifierExceptionList::Matches(nsIURI* aURI, nsIURI* aTopLevelURI,
                                    bool aIsPrivateBrowsing, bool* aResult) {
  NS_ENSURE_ARG_POINTER(aURI);
  NS_ENSURE_ARG_POINTER(aResult);

  // Record how long it takes to perform the exception list lookup.
  AUTO_PROFILER_MARKER_UNTYPED("UrlClassifierExceptionList::Matches", OTHER,
                               MarkerTiming::IntervalStart());

  *aResult = false;

  UC_LOG_DEBUG(
      ("UrlClassifierExceptionList::%s - aURI: %s, aTopLevelURI: %s, "
       "aIsPrivateBrowsing: %d",
       __FUNCTION__, aURI->GetSpecOrDefault().get(),
       aTopLevelURI ? aTopLevelURI->GetSpecOrDefault().get() : "null",
       aIsPrivateBrowsing));

  // Get the eTLD service so we can compute sites from URIs.
  nsresult rv;
  nsCOMPtr<nsIEffectiveTLDService> eTLDService(
      do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID, &rv));
  NS_ENSURE_SUCCESS(rv, rv);

  // If given, compute the (schemeless) site from the top level URI.
  // If not we will leave it empty and only look for global exceptions.
  nsAutoCString aTopLevelSite;
  if (aTopLevelURI) {
    rv = eTLDService->GetSchemelessSite(aTopLevelURI, aTopLevelSite);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  // Compute the (schemeless) site from the URI of the load.
  nsAutoCString aSite;
  rv = eTLDService->GetSchemelessSite(aURI, aSite);
  NS_ENSURE_SUCCESS(rv, rv);

  // Get the list of exceptions that apply to the current load.
  // We need to check both global and site specific exceptions

  // 1. Check global exceptions, which apply to all top level sites and lookup
  //    entries matching the current load (aSite).
  ExceptionEntryArray* globalExceptions =
      mGlobalExceptions.Lookup(aSite).DataPtrOrNull();

  *aResult = ExceptionListMatchesLoad(globalExceptions, aURI, aTopLevelURI,
                                      aIsPrivateBrowsing);
  if (*aResult) {
    // We found a match, no need to check the site specific exceptions.
    return NS_OK;
  }

  // 2. Get exceptions which apply only to the current top level site.
  ExceptionEntryArray* siteSpecificExceptions =
      mExceptions.Lookup(aTopLevelSite).DataPtrOrNull();
  if (siteSpecificExceptions) {
    *aResult = ExceptionListMatchesLoad(siteSpecificExceptions, aURI,
                                        aTopLevelURI, aIsPrivateBrowsing);
    if (*aResult) {
      return NS_OK;
    }
  }

  if (!(*aResult)) {
    UC_LOG_DEBUG(("%s - No match found", __FUNCTION__));
  }

  return NS_OK;
}

bool UrlClassifierExceptionList::ExceptionListMatchesLoad(
    ExceptionEntryArray* aExceptions, nsIURI* aURI, nsIURI* aTopLevelURI,
    bool aIsPrivateBrowsing) {
  MOZ_ASSERT(aURI);

  if (!aExceptions) {
    return false;
  }
  for (const auto& entry : *aExceptions) {
    bool match = false;
    nsresult rv =
        entry->Matches(aURI, aTopLevelURI, aIsPrivateBrowsing, &match);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      continue;
    }
    if (match) {
      // Match found, return immediately.
      if (MOZ_LOG_TEST(gChannelClassifierLog, LogLevel::Debug)) {
        nsAutoCString entryString;
        (void)entry->Describe(entryString);
        UC_LOG_DEBUG(
            ("UrlClassifierExceptionList::%s - Exception list match found. "
             "entry: %s",
             __FUNCTION__, entryString.get()));
      }
      return true;
    }
  }
  return false;
}

NS_IMETHODIMP
UrlClassifierExceptionList::GetSchemelessSiteFromUrlPattern(
    const nsACString& aUrlPattern, nsACString& aSite) {
  if (aUrlPattern.IsEmpty()) {
    aSite.Truncate();
    return NS_OK;
  }

  // Extract the host portion from the url pattern. This regex only supports url
  // patterns with a host.
  mozilla::RustRegex regex("://(?:\\*\\.)?([^/*]+)");
  mozilla::RustRegexCaptures captures = regex.FindCaptures(aUrlPattern);
  NS_ENSURE_TRUE(captures.IsValid(), NS_ERROR_INVALID_ARG);

  // Get the host from the first capture group
  auto maybeMatch = captures[1];
  NS_ENSURE_TRUE(maybeMatch, NS_ERROR_INVALID_ARG);

  nsAutoCString host;
  host.Assign(Substring(aUrlPattern, maybeMatch->start,
                        maybeMatch->end - maybeMatch->start));
  NS_ENSURE_TRUE(!host.IsEmpty(), NS_ERROR_INVALID_ARG);

  // Get the eTLD service to convert host to schemeless site
  nsresult rv;
  nsCOMPtr<nsIEffectiveTLDService> eTLDService(
      do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID, &rv));
  NS_ENSURE_SUCCESS(rv, rv);

  return eTLDService->GetSchemelessSiteFromHost(host, aSite);
}

NS_IMETHODIMP
UrlClassifierExceptionList::TestGetEntries(
    nsTArray<RefPtr<nsIUrlClassifierExceptionListEntry>>& aEntries) {
  // Global entries (not top-level specific)
  for (const auto& entry : mGlobalExceptions) {
    const ExceptionEntryArray& entries = entry.GetData();
    aEntries.AppendElements(entries);
  }

  // Site specific entries.
  for (const auto& entry : mExceptions) {
    const ExceptionEntryArray& entries = entry.GetData();
    // Append all entries from this array to the result
    aEntries.AppendElements(entries);
  }

  return NS_OK;
}
}  // namespace mozilla::net
