/* 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 "mozilla/dom/ReportingHeader.h"

#include <limits>

#include "js/Array.h"  // JS::GetArrayLength, JS::IsArrayObject
#include "js/GCAPI.h"
#include "js/JSON.h"
#include "js/PropertyAndElement.h"  // JS_GetElement
#include "mozilla/OriginAttributes.h"
#include "mozilla/Services.h"
#include "mozilla/StaticPrefs_dom.h"
#include "mozilla/StaticPtr.h"
#include "mozilla/dom/ReportingBinding.h"
#include "mozilla/dom/ScriptSettings.h"
#include "mozilla/dom/SimpleGlobalObject.h"
#include "mozilla/ipc/BackgroundUtils.h"
#include "mozilla/net/SFV.h"
#include "nsCOMPtr.h"
#include "nsContentUtils.h"
#include "nsIEffectiveTLDService.h"
#include "nsIHttpChannel.h"
#include "nsIHttpProtocolHandler.h"
#include "nsIObserverService.h"
#include "nsIPrincipal.h"
#include "nsIRandomGenerator.h"
#include "nsIScriptError.h"
#include "nsMixedContentBlocker.h"
#include "nsNetUtil.h"
#include "nsXULAppAPI.h"

#define REPORTING_PURGE_ALL "reporting:purge-all"
#define REPORTING_PURGE_HOST "reporting:purge-host"

namespace mozilla::dom {

namespace {

StaticRefPtr<ReportingHeader> gReporting;

}  // namespace

/* static */
void ReportingHeader::Initialize() {
  MOZ_ASSERT(!gReporting);
  MOZ_ASSERT(NS_IsMainThread());

  if (!XRE_IsParentProcess()) {
    return;
  }

  RefPtr<ReportingHeader> service = new ReportingHeader();

  nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
  if (NS_WARN_IF(!obs)) {
    return;
  }

  obs->AddObserver(service, NS_HTTP_ON_EXAMINE_RESPONSE_TOPIC, false);
  obs->AddObserver(service, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
  obs->AddObserver(service, "clear-origin-attributes-data", false);
  obs->AddObserver(service, REPORTING_PURGE_HOST, false);
  obs->AddObserver(service, REPORTING_PURGE_ALL, false);

  gReporting = service;
}

/* static */
void ReportingHeader::Shutdown() {
  MOZ_ASSERT(NS_IsMainThread());

  if (!gReporting) {
    return;
  }

  RefPtr<ReportingHeader> service = gReporting;
  gReporting = nullptr;

  if (service->mCleanupTimer) {
    service->mCleanupTimer->Cancel();
    service->mCleanupTimer = nullptr;
  }

  nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
  if (NS_WARN_IF(!obs)) {
    return;
  }

  obs->RemoveObserver(service, NS_HTTP_ON_EXAMINE_RESPONSE_TOPIC);
  obs->RemoveObserver(service, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
  obs->RemoveObserver(service, "clear-origin-attributes-data");
  obs->RemoveObserver(service, REPORTING_PURGE_HOST);
  obs->RemoveObserver(service, REPORTING_PURGE_ALL);
}

ReportingHeader::ReportingHeader() = default;
ReportingHeader::~ReportingHeader() = default;

NS_IMETHODIMP
ReportingHeader::Observe(nsISupports* aSubject, const char* aTopic,
                         const char16_t* aData) {
  if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) {
    Shutdown();
    return NS_OK;
  }

  // Pref disabled.
  if (!StaticPrefs::dom_reporting_header_enabled()) {
    return NS_OK;
  }

  if (!strcmp(aTopic, NS_HTTP_ON_EXAMINE_RESPONSE_TOPIC)) {
    nsCOMPtr<nsIHttpChannel> channel = do_QueryInterface(aSubject);
    if (NS_WARN_IF(!channel)) {
      return NS_OK;
    }

    ReportingFromChannel(channel);
    return NS_OK;
  }

  if (!strcmp(aTopic, REPORTING_PURGE_HOST)) {
    RemoveOriginsFromHost(nsDependentString(aData));
    return NS_OK;
  }

  if (!strcmp(aTopic, "clear-origin-attributes-data")) {
    OriginAttributesPattern pattern;
    if (!pattern.Init(nsDependentString(aData))) {
      NS_ERROR("Cannot parse origin attributes pattern");
      return NS_ERROR_FAILURE;
    }

    RemoveOriginsFromOriginAttributesPattern(pattern);
    return NS_OK;
  }

  if (!strcmp(aTopic, REPORTING_PURGE_ALL)) {
    RemoveOrigins();
    return NS_OK;
  }

  return NS_ERROR_FAILURE;
}

void ReportingHeader::ReportingFromChannel(nsIHttpChannel* aChannel) {
  MOZ_ASSERT(aChannel);

  if (!StaticPrefs::dom_reporting_header_enabled()) {
    return;
  }

  // We want to use the final URI to check if Report-To should be allowed or
  // not.
  nsCOMPtr<nsIURI> uri;
  nsresult rv = aChannel->GetURI(getter_AddRefs(uri));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return;
  }

  if (!nsMixedContentBlocker::IsPotentiallyTrustworthyOrigin(uri)) {
    return;
  }

  if (NS_UsePrivateBrowsing(aChannel)) {
    return;
  }

  nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager();
  if (NS_WARN_IF(!ssm)) {
    return;
  }

  nsCOMPtr<nsIPrincipal> principal;
  rv = ssm->GetChannelURIPrincipal(aChannel, getter_AddRefs(principal));
  if (NS_WARN_IF(NS_FAILED(rv)) || !principal) {
    return;
  }

  nsAutoCString origin;
  rv = principal->GetOrigin(origin);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return;
  }

  // Parse Report-To and Reporting-Endpoints headers
  UniquePtr<Client> client;
  nsAutoCString header;

  if (NS_SUCCEEDED(
          aChannel->GetResponseHeader("Reporting-Endpoints"_ns, header))) {
    client = MakeUnique<Client>();
    size_t parsedItems = ParseReportingEndpointsHeader(
        header, uri, [&](const nsAString& aKey, nsCOMPtr<nsIURI> aEndpointUrl) {
          Group* group = client->mGroups.AppendElement();
          group->mCreationTime = TimeStamp::Now();
          group->mTTL = std::numeric_limits<int32_t>::max();
          group->mName = aKey;

          // Use data extracted from dictionary entry to create an endpoint
          group->mEndpoints.AppendElement(
              Endpoint::Create(aEndpointUrl.forget(), aKey));
        });
    if (parsedItems == 0) {
      client = nullptr;
    }
  } else if (NS_SUCCEEDED(
                 aChannel->GetResponseHeader("Report-To"_ns, header))) {
    client = ParseReportToHeader(aChannel, uri, header);
  }

  if (!client) {
    return;
  }

  // Here we override the previous data.
  mOrigins.InsertOrUpdate(origin, std::move(client));

  MaybeCreateCleanupTimer();
}

/* static */
EndpointsList ReportingHeader::ProcessReportingEndpointsListFromResponse(
    nsIHttpChannel* aChannel) {
  if (!StaticPrefs::dom_reporting_enabled()) {
    return {};
  }

  // We want to use the final URI to check if Report-To should be allowed or
  // not.
  nsCOMPtr<nsIURI> uri;
  nsresult rv = aChannel->GetURI(getter_AddRefs(uri));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return {};
  }

  if (!nsMixedContentBlocker::IsPotentiallyTrustworthyOrigin(uri)) {
    return {};
  }

  if (NS_UsePrivateBrowsing(aChannel)) {
    return {};
  }

  nsAutoCString header;
  EndpointsList result;

  // Note: Legacy Report-To header supported by very few browsers
  // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Report-To
  if (NS_SUCCEEDED(
          aChannel->GetResponseHeader("Reporting-Endpoints"_ns, header))) {
    (void)ParseReportingEndpointsHeader(
        header, uri, [&](const nsAString& aKey, nsCOMPtr<nsIURI> aEndpointURL) {
          result.mData.EmplaceBack(
              Endpoint::Create(aEndpointURL.forget(), aKey));
        });
  }
  return result;
}

/* static */
size_t ReportingHeader::ParseReportingEndpointsHeader(
    const nsACString& aHeaderValue, nsIURI* aURI,
    std::function<void(const nsAString&, nsCOMPtr<nsIURI>)>&&
        aOnParsedItemCallback) {
  nsAutoCString uriSpec;
  aURI->GetSpec(uriSpec);

  nsCOMPtr<nsIURI> baseURL;
  if (NS_FAILED(NS_NewURI(getter_AddRefs(baseURL), uriSpec))) {
    return 0;
  }

  auto dict = mozilla::net::SFV::ParseDict(aHeaderValue);
  if (!dict.IsValid()) {
    return 0;
  }

  nsTArray<nsCString> keys;
  if (NS_FAILED(dict.GetKeys(keys))) {
    return 0;
  }

  size_t itemsParsed = 0;

  if (!nsMixedContentBlocker::IsPotentiallyTrustworthyOrigin(aURI)) {
    return 0;
  }

  for (const auto& key : keys) {
    nsAutoCString endpointURLString;

    // Try to get value as a direct string item
    if (NS_SUCCEEDED(dict.GetItem<mozilla::net::SFV::SFVString>(
            key, endpointURLString))) {
      // Got a direct string item
    } else {
      // Try to get value as an inner list and extract first item
      auto innerList = dict.GetInnerList(key);
      if (!innerList.IsValid() || innerList.Length() == 0) {
        continue;
      }

      auto firstItem = innerList.GetItemAt(0);
      if (!firstItem.IsValid()) {
        continue;
      }

      if (NS_FAILED(firstItem.GetValue<mozilla::net::SFV::SFVString>(
              endpointURLString))) {
        continue;
      }
    }

    nsCOMPtr<nsIURI> endpointURL;
    nsresult rv = NS_NewURI(getter_AddRefs(endpointURL),
                            endpointURLString.get(), baseURL);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      continue;
    }

    if (!nsMixedContentBlocker::IsPotentiallyTrustworthyOrigin(endpointURL)) {
      continue;
    }

    ++itemsParsed;
    aOnParsedItemCallback(NS_ConvertUTF8toUTF16(key), std::move(endpointURL));
  }

  return itemsParsed;
}

/* static */ UniquePtr<ReportingHeader::Client>
ReportingHeader::ParseReportToHeader(nsIHttpChannel* aChannel, nsIURI* aURI,
                                     const nsACString& aHeaderValue) {
  MOZ_ASSERT(aURI);
  // aChannel can be null in gtest

  AutoJSAPI jsapi;

  JSObject* cleanGlobal =
      SimpleGlobalObject::Create(SimpleGlobalObject::GlobalType::BindingDetail);
  if (NS_WARN_IF(!cleanGlobal)) {
    return nullptr;
  }

  if (NS_WARN_IF(!jsapi.Init(cleanGlobal))) {
    return nullptr;
  }

  // WebIDL dictionary parses single items. Let's create a object to parse the
  // header.
  nsAutoString json;
  json.AppendASCII("{ \"items\": [");
  json.Append(NS_ConvertUTF8toUTF16(aHeaderValue));
  json.AppendASCII("]}");

  JSContext* cx = jsapi.cx();
  JS::Rooted<JS::Value> jsonValue(cx);
  bool ok = JS_ParseJSON(cx, json.BeginReading(), json.Length(), &jsonValue);
  if (!ok) {
    // Drop error in favor of logging a generic message, because the error
    // message's column numbers are confusing due to the prepended characters
    // above, and a user cannot do much about it anyway (bug 2020662).
    JS_ClearPendingException(cx);
    LogToConsoleInvalidJSON(aChannel, aURI);
    return nullptr;
  }

  RootedDictionary<dom::ReportingHeaderValue> data(cx);
  if (!data.Init(cx, jsonValue)) {
    // Ignore error in favor of generic message to avoid logspam (bug 2020662).
    JS_ClearPendingException(cx);
    LogToConsoleInvalidJSON(aChannel, aURI);
    return nullptr;
  }

  if (!data.mItems.WasPassed() || data.mItems.Value().IsEmpty()) {
    return nullptr;
  }

  UniquePtr<Client> client = MakeUnique<Client>();

  for (const dom::ReportingItem& item : data.mItems.Value()) {
    nsAutoString groupName;

    if (item.mGroup.isUndefined()) {
      groupName.AssignLiteral("default");
    } else if (!item.mGroup.isString()) {
      LogToConsoleInvalidNameItem(aChannel, aURI);
      continue;
    } else {
      JS::Rooted<JSString*> groupStr(cx, item.mGroup.toString());
      MOZ_ASSERT(groupStr);

      nsAutoJSString string;
      if (NS_WARN_IF(!string.init(cx, groupStr))) {
        continue;
      }

      groupName = string;
    }

    if (!item.mMax_age.isNumber() || !item.mEndpoints.isObject()) {
      LogToConsoleIncompleteItem(aChannel, aURI, groupName);
      continue;
    }

    JS::Rooted<JSObject*> endpoints(cx, &item.mEndpoints.toObject());
    MOZ_ASSERT(endpoints);

    bool isArray = false;
    if (!JS::IsArrayObject(cx, endpoints, &isArray) || !isArray) {
      LogToConsoleIncompleteItem(aChannel, aURI, groupName);
      continue;
    }

    uint32_t endpointsLength;
    if (!JS::GetArrayLength(cx, endpoints, &endpointsLength) ||
        endpointsLength == 0) {
      // TODO: should this clear the endpoint instead?
      LogToConsoleIncompleteItem(aChannel, aURI, groupName);
      continue;
    }

    const auto [begin, end] = client->mGroups.NonObservingRange();
    if (std::any_of(begin, end, [&groupName](const Group& group) {
          return group.mName == groupName;
        })) {
      LogToConsoleDuplicateGroup(aChannel, aURI, groupName);
      continue;
    }

    Group* group = client->mGroups.AppendElement();
    group->mName = groupName;
    group->mIncludeSubdomains = item.mInclude_subdomains;
    group->mTTL = item.mMax_age.toNumber();
    group->mCreationTime = TimeStamp::Now();

    for (uint32_t i = 0; i < endpointsLength; ++i) {
      JS::Rooted<JS::Value> element(cx);
      if (!JS_GetElement(cx, endpoints, i, &element)) {
        return nullptr;
      }

      RootedDictionary<ReportingEndpoint> endpoint(cx);
      if (!endpoint.Init(cx, element)) {
        LogToConsoleIncompleteEndpoint(aChannel, aURI, groupName);
        continue;
      }

      if (!endpoint.mUrl.isString() ||
          (!endpoint.mPriority.isUndefined() &&
           (!endpoint.mPriority.isNumber() ||
            endpoint.mPriority.toNumber() < 0)) ||
          (!endpoint.mWeight.isUndefined() &&
           (!endpoint.mWeight.isNumber() || endpoint.mWeight.toNumber() < 0))) {
        LogToConsoleIncompleteEndpoint(aChannel, aURI, groupName);
        continue;
      }

      JS::Rooted<JSString*> endpointUrl(cx, endpoint.mUrl.toString());
      MOZ_ASSERT(endpointUrl);

      nsAutoJSString endpointString;
      if (NS_WARN_IF(!endpointString.init(cx, endpointUrl))) {
        continue;
      }

      nsCOMPtr<nsIURI> uri;
      nsresult rv = NS_NewURI(getter_AddRefs(uri), endpointString);
      if (NS_FAILED(rv)) {
        LogToConsoleInvalidURLEndpoint(aChannel, aURI, groupName,
                                       endpointString);
        continue;
      }

      Endpoint* ep = group->mEndpoints.AppendElement();
      ep->mUrl = uri;
      ep->mPriority =
          endpoint.mPriority.isUndefined() ? 1 : endpoint.mPriority.toNumber();
      ep->mWeight =
          endpoint.mWeight.isUndefined() ? 1 : endpoint.mWeight.toNumber();
    }
  }

  if (client->mGroups.IsEmpty()) {
    return nullptr;
  }

  return client;
}

/* static */
void ReportingHeader::LogToConsoleInvalidJSON(nsIHttpChannel* aChannel,
                                              nsIURI* aURI) {
  nsTArray<nsString> params;
  LogToConsoleInternal(aChannel, aURI, "ReportingHeaderInvalidJSON", params);
}

/* static */
void ReportingHeader::LogToConsoleDuplicateGroup(nsIHttpChannel* aChannel,
                                                 nsIURI* aURI,
                                                 const nsAString& aName) {
  nsTArray<nsString> params;
  params.AppendElement(aName);

  LogToConsoleInternal(aChannel, aURI, "ReportingHeaderDuplicateGroup", params);
}

/* static */
void ReportingHeader::LogToConsoleInvalidNameItem(nsIHttpChannel* aChannel,
                                                  nsIURI* aURI) {
  nsTArray<nsString> params;
  LogToConsoleInternal(aChannel, aURI, "ReportingHeaderInvalidNameItem",
                       params);
}

/* static */
void ReportingHeader::LogToConsoleIncompleteItem(nsIHttpChannel* aChannel,
                                                 nsIURI* aURI,
                                                 const nsAString& aName) {
  nsTArray<nsString> params;
  params.AppendElement(aName);

  LogToConsoleInternal(aChannel, aURI, "ReportingHeaderInvalidItem", params);
}

/* static */
void ReportingHeader::LogToConsoleIncompleteEndpoint(nsIHttpChannel* aChannel,
                                                     nsIURI* aURI,
                                                     const nsAString& aName) {
  nsTArray<nsString> params;
  params.AppendElement(aName);

  LogToConsoleInternal(aChannel, aURI, "ReportingHeaderInvalidEndpoint",
                       params);
}

/* static */
void ReportingHeader::LogToConsoleInvalidURLEndpoint(nsIHttpChannel* aChannel,
                                                     nsIURI* aURI,
                                                     const nsAString& aName,
                                                     const nsAString& aURL) {
  nsTArray<nsString> params;
  params.AppendElement(aURL);
  params.AppendElement(aName);

  LogToConsoleInternal(aChannel, aURI, "ReportingHeaderInvalidURLEndpoint",
                       params);
}

/* static */
void ReportingHeader::LogToConsoleInternal(nsIHttpChannel* aChannel,
                                           nsIURI* aURI, const char* aMsg,
                                           const nsTArray<nsString>& aParams) {
  MOZ_ASSERT(aURI);

  if (!aChannel) {
    // We are in a gtest.
    return;
  }

  uint64_t windowID = 0;

  nsresult rv = aChannel->GetTopLevelContentWindowId(&windowID);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return;
  }

  if (!windowID) {
    nsCOMPtr<nsILoadGroup> loadGroup;
    nsresult rv = aChannel->GetLoadGroup(getter_AddRefs(loadGroup));
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return;
    }

    if (loadGroup) {
      windowID = nsContentUtils::GetInnerWindowID(loadGroup);
    }
  }

  nsAutoString localizedMsg;
  rv = nsContentUtils::FormatLocalizedString(
      PropertiesFile::SECURITY_PROPERTIES, aMsg, aParams, localizedMsg);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return;
  }

  rv = nsContentUtils::ReportToConsoleByWindowID(
      localizedMsg, nsIScriptError::infoFlag, "Reporting"_ns, windowID,
      SourceLocation(aURI));
  (void)NS_WARN_IF(NS_FAILED(rv));
}

/* static */
void ReportingHeader::GetEndpointForReport(
    const nsAString& aGroupName,
    const mozilla::ipc::PrincipalInfo& aPrincipalInfo,
    nsACString& aEndpointURI) {
  auto principalOrErr = PrincipalInfoToPrincipal(aPrincipalInfo);
  if (NS_WARN_IF(principalOrErr.isErr())) {
    return;
  }

  nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap();
  GetEndpointForReport(aGroupName, principal, aEndpointURI);
}

/* static */
void ReportingHeader::GetEndpointForReport(const nsAString& aGroupName,
                                           nsIPrincipal* aPrincipal,
                                           nsACString& aEndpointURI) {
  return GetEndpointForReportIncludeSubdomains(
      aGroupName, aPrincipal, /* includeSubdomains */ false, aEndpointURI);
}
/* static */
void ReportingHeader::GetEndpointForReportIncludeSubdomains(
    const nsAString& aGroupName, nsIPrincipal* aPrincipal,
    bool aIncludeSubdomains, nsACString& aEndpointURI) {
  MOZ_ASSERT(aEndpointURI.IsEmpty());

  if (!gReporting) {
    return;
  }

  nsCOMPtr<nsIPrincipal> principal = aPrincipal;
  bool mustHaveIncludeSubdomains = false;

  do {
    nsAutoCString origin;
    nsresult rv = principal->GetOrigin(origin);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return;
    }

    Client* client = gReporting->mOrigins.Get(origin);
    if (client) {
      const auto [begin, end] = client->mGroups.NonObservingRange();
      const auto foundIt = std::find_if(
          begin, end,
          [&aGroupName, mustHaveIncludeSubdomains](const Group& group) {
            return group.mName == aGroupName &&
                   (!mustHaveIncludeSubdomains || group.mIncludeSubdomains);
          });
      if (foundIt != end) {
        GetEndpointForReportInternal(*foundIt, aEndpointURI);
        return;
      }
    }

    nsCOMPtr<nsIPrincipal> oldPrincipal = std::move(principal);
    oldPrincipal->GetNextSubDomainPrincipal(getter_AddRefs(principal));
    mustHaveIncludeSubdomains = true;
  } while (principal && aIncludeSubdomains);

  // XXX More explicitly report an error if not found?
}

/* static */
void ReportingHeader::GetEndpointForReportInternal(
    const ReportingHeader::Group& aGroup, nsACString& aEndpointURI) {
  TimeDuration diff = TimeStamp::Now() - aGroup.mCreationTime;
  if (diff.ToSeconds() > aGroup.mTTL) {
    // Expired.
    return;
  }

  if (aGroup.mEndpoints.IsEmpty()) {
    return;
  }

  int64_t minPriority = -1;
  uint32_t totalWeight = 0;

  for (const Endpoint& endpoint : aGroup.mEndpoints.NonObservingRange()) {
    if (minPriority == -1 || minPriority > endpoint.mPriority) {
      minPriority = endpoint.mPriority;
      totalWeight = endpoint.mWeight;
    } else if (minPriority == endpoint.mPriority) {
      totalWeight += endpoint.mWeight;
    }
  }

  nsCOMPtr<nsIRandomGenerator> randomGenerator =
      do_GetService("@mozilla.org/security/random-generator;1");
  if (NS_WARN_IF(!randomGenerator)) {
    return;
  }

  uint32_t randomNumber = 0;

  nsresult rv = randomGenerator->GenerateRandomBytesInto(randomNumber);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return;
  }

  totalWeight = randomNumber % totalWeight;

  const auto [begin, end] = aGroup.mEndpoints.NonObservingRange();
  const auto foundIt = std::find_if(
      begin, end, [minPriority, totalWeight](const Endpoint& endpoint) {
        return minPriority == endpoint.mPriority &&
               totalWeight < endpoint.mWeight;
      });
  if (foundIt != end) {
    (void)NS_WARN_IF(NS_FAILED(foundIt->mUrl->GetSpec(aEndpointURI)));
  }
  // XXX More explicitly report an error if not found?
}

/* static */
void ReportingHeader::RemoveEndpoint(const nsAString& aGroupName,
                                     const nsACString& aEndpointURL,
                                     nsIPrincipal* aPrincipal) {
  if (!gReporting) {
    return;
  }

  nsCOMPtr<nsIURI> uri;
  nsresult rv = NS_NewURI(getter_AddRefs(uri), aEndpointURL);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return;
  }

  if (NS_WARN_IF(!aPrincipal)) {
    return;
  }

  nsAutoCString origin;
  rv = aPrincipal->GetOrigin(origin);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return;
  }

  Client* client = gReporting->mOrigins.Get(origin);
  if (!client) {
    return;
  }

  // Scope for the group iterator.
  {
    nsTObserverArray<Group>::BackwardIterator iter(client->mGroups);
    while (iter.HasMore()) {
      const Group& group = iter.GetNext();
      if (group.mName != aGroupName) {
        continue;
      }

      // Scope for the endpoint iterator.
      {
        nsTObserverArray<Endpoint>::BackwardIterator endpointIter(
            group.mEndpoints);
        while (endpointIter.HasMore()) {
          const Endpoint& endpoint = endpointIter.GetNext();

          bool equal = false;
          rv = endpoint.mUrl->Equals(uri, &equal);
          if (NS_WARN_IF(NS_FAILED(rv))) {
            continue;
          }

          if (equal) {
            endpointIter.Remove();
            break;
          }
        }
      }

      if (group.mEndpoints.IsEmpty()) {
        iter.Remove();
      }

      break;
    }
  }

  if (client->mGroups.IsEmpty()) {
    gReporting->mOrigins.Remove(origin);
    gReporting->MaybeCancelCleanupTimer();
  }
}

void ReportingHeader::RemoveOriginsFromHost(const nsAString& aHost) {
  nsCOMPtr<nsIEffectiveTLDService> tldService =
      do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID);
  if (NS_WARN_IF(!tldService)) {
    return;
  }

  NS_ConvertUTF16toUTF8 host(aHost);

  for (auto iter = mOrigins.Iter(); !iter.Done(); iter.Next()) {
    bool hasRootDomain = false;
    nsresult rv = tldService->HasRootDomain(iter.Key(), host, &hasRootDomain);
    if (NS_WARN_IF(NS_FAILED(rv)) || !hasRootDomain) {
      continue;
    }

    iter.Remove();
  }

  MaybeCancelCleanupTimer();
}

void ReportingHeader::RemoveOriginsFromOriginAttributesPattern(
    const OriginAttributesPattern& aPattern) {
  for (auto iter = mOrigins.Iter(); !iter.Done(); iter.Next()) {
    nsAutoCString suffix;
    OriginAttributes attr;
    if (NS_WARN_IF(!attr.PopulateFromOrigin(iter.Key(), suffix))) {
      continue;
    }

    if (aPattern.Matches(attr)) {
      iter.Remove();
    }
  }

  MaybeCancelCleanupTimer();
}

void ReportingHeader::RemoveOrigins() {
  mOrigins.Clear();
  MaybeCancelCleanupTimer();
}

void ReportingHeader::RemoveOriginsForTTL() {
  TimeStamp now = TimeStamp::Now();

  for (auto iter = mOrigins.Iter(); !iter.Done(); iter.Next()) {
    Client* client = iter.UserData();

    // Scope of the iterator.
    {
      nsTObserverArray<Group>::BackwardIterator groupIter(client->mGroups);
      while (groupIter.HasMore()) {
        const Group& group = groupIter.GetNext();
        TimeDuration diff = now - group.mCreationTime;
        if (diff.ToSeconds() > group.mTTL) {
          groupIter.Remove();
          return;
        }
      }
    }

    if (client->mGroups.IsEmpty()) {
      iter.Remove();
    }
  }
}

ReportingHeader::Endpoint* EndpointsList::GetEndpointWithName(
    const nsAString& aEndpointName) {
  for (auto& endpoint : mData) {
    if (endpoint.mEndpointName == aEndpointName) {
      return &endpoint;
    }
  }
  return nullptr;
}

void EndpointsList::RemoveEndpoint(const nsAString& aEndpointName) {
  const auto it = std::ranges::find_if(
      mData, [&aEndpointName](const ReportingHeader::Endpoint& aEndpoint) {
        return aEndpoint.mEndpointName == aEndpointName;
      });
  if (it != std::end(mData)) {
    mData.RemoveElementAt(it);
  }
}

/* static */
bool ReportingHeader::HasReportingHeaderForOrigin(const nsACString& aOrigin) {
  if (!gReporting) {
    return false;
  }

  return gReporting->mOrigins.Contains(aOrigin);
}

NS_IMETHODIMP
ReportingHeader::Notify(nsITimer* aTimer) {
  mCleanupTimer = nullptr;

  RemoveOriginsForTTL();
  MaybeCreateCleanupTimer();

  return NS_OK;
}

NS_IMETHODIMP
ReportingHeader::GetName(nsACString& aName) {
  aName.AssignLiteral("ReportingHeader");
  return NS_OK;
}

void ReportingHeader::MaybeCreateCleanupTimer() {
  if (mCleanupTimer) {
    return;
  }

  if (mOrigins.Count() == 0) {
    return;
  }

  uint32_t timeout = StaticPrefs::dom_reporting_cleanup_timeout() * 1000;
  nsresult rv =
      NS_NewTimerWithCallback(getter_AddRefs(mCleanupTimer), this, timeout,
                              nsITimer::TYPE_ONE_SHOT_LOW_PRIORITY);
  (void)NS_WARN_IF(NS_FAILED(rv));
}

void ReportingHeader::MaybeCancelCleanupTimer() {
  if (!mCleanupTimer) {
    return;
  }

  if (mOrigins.Count() != 0) {
    return;
  }

  mCleanupTimer->Cancel();
  mCleanupTimer = nullptr;
}

NS_INTERFACE_MAP_BEGIN(ReportingHeader)
  NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver)
  NS_INTERFACE_MAP_ENTRY(nsIObserver)
  NS_INTERFACE_MAP_ENTRY(nsITimerCallback)
  NS_INTERFACE_MAP_ENTRY(nsINamed)
NS_INTERFACE_MAP_END

NS_IMPL_ADDREF(ReportingHeader)
NS_IMPL_RELEASE(ReportingHeader)

}  // namespace mozilla::dom
