/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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 "Sanitizer.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/StaticPtr.h"
#include "mozilla/dom/BindingDeclarations.h"
#include "mozilla/dom/BindingUtils.h"
#include "mozilla/dom/DocumentFragment.h"
#include "mozilla/dom/HTMLTemplateElement.h"
#include "mozilla/dom/SanitizerBinding.h"
#include "mozilla/dom/SanitizerDefaultConfig.h"
#include "nsContentUtils.h"
#include "nsGenericHTMLElement.h"
#include "nsIContentInlines.h"
#include "nsNameSpaceManager.h"

namespace mozilla::dom {

using namespace sanitizer;

NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(Sanitizer, mGlobal)

NS_IMPL_CYCLE_COLLECTING_ADDREF(Sanitizer)
NS_IMPL_CYCLE_COLLECTING_RELEASE(Sanitizer)

NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Sanitizer)
  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
  NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END

// Map[ElementName -> ?Set[Attributes]]
using ElementsWithAttributes =
    nsTHashMap<const nsStaticAtom*, UniquePtr<StaticAtomSet>>;

StaticAutoPtr<ElementsWithAttributes> sDefaultHTMLElements;
StaticAutoPtr<ElementsWithAttributes> sDefaultMathMLElements;
StaticAutoPtr<StaticAtomSet> sDefaultAttributes;

JSObject* Sanitizer::WrapObject(JSContext* aCx,
                                JS::Handle<JSObject*> aGivenProto) {
  return Sanitizer_Binding::Wrap(aCx, this, aGivenProto);
}

/* static */
// https://wicg.github.io/sanitizer-api/#sanitizerconfig-get-a-sanitizer-instance-from-options
already_AddRefed<Sanitizer> Sanitizer::GetInstance(
    nsIGlobalObject* aGlobal,
    const OwningSanitizerOrSanitizerConfigOrSanitizerPresets& aOptions,
    bool aSafe, ErrorResult& aRv) {
  // Step 4. If sanitizerSpec is a string:
  if (aOptions.IsSanitizerPresets()) {
    // Step 4.1. Assert: sanitizerSpec is "default"
    MOZ_ASSERT(aOptions.GetAsSanitizerPresets() == SanitizerPresets::Default);

    // Step 4.2. Set sanitizerSpec to the built-in safe default configuration.
    // NOTE: The built-in safe default configuration is complete and not
    // influenced by |safe|.
    RefPtr<Sanitizer> sanitizer = new Sanitizer(aGlobal);
    sanitizer->SetDefaultConfig();
    return sanitizer.forget();
  }

  // Step 5. Assert: sanitizerSpec is either a Sanitizer instance, or a
  // dictionary. Step 6. If sanitizerSpec is a dictionary:
  if (aOptions.IsSanitizerConfig()) {
    // Step 6.1. Let sanitizer be a new Sanitizer instance.
    RefPtr<Sanitizer> sanitizer = new Sanitizer(aGlobal);

    // Step 6.2. Let setConfigurationResult be the result of set a configuration
    // with sanitizerSpec and not safe on sanitizer.
    sanitizer->SetConfig(aOptions.GetAsSanitizerConfig(), !aSafe, aRv);

    // Step 6.3. If setConfigurationResult is false, throw a TypeError.
    if (aRv.Failed()) {
      return nullptr;
    }

    // Step 6.4. Set sanitizerSpec to sanitizer.
    return sanitizer.forget();
  }

  // Step 7. Assert: sanitizerSpec is a Sanitizer instance.
  MOZ_ASSERT(aOptions.IsSanitizer());

  // Step 8. Return sanitizerSpec.
  RefPtr<Sanitizer> sanitizer = aOptions.GetAsSanitizer();
  return sanitizer.forget();
}

/* static */
// https://wicg.github.io/sanitizer-api/#sanitizer-constructor
already_AddRefed<Sanitizer> Sanitizer::Constructor(
    const GlobalObject& aGlobal,
    const SanitizerConfigOrSanitizerPresets& aConfig, ErrorResult& aRv) {
  nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
  RefPtr<Sanitizer> sanitizer = new Sanitizer(global);

  // Step 1. If configuration is a SanitizerPresets string, then:
  if (aConfig.IsSanitizerPresets()) {
    // Step 1.1. Assert: configuration is default.
    MOZ_ASSERT(aConfig.GetAsSanitizerPresets() == SanitizerPresets::Default);

    // Step 1.2. Set configuration to the built-in safe default configuration .
    sanitizer->SetDefaultConfig();

    // NOTE: Early return because we don't need to do any
    // processing/verification of the default config.
    return sanitizer.forget();
  }

  // Step 2. Let valid be the return value of set a configuration with
  // configuration and true on this.
  sanitizer->SetConfig(aConfig.GetAsSanitizerConfig(), true, aRv);

  // Step 3. If valid is false, then throw a TypeError.
  if (aRv.Failed()) {
    return nullptr;
  }

  return sanitizer.forget();
}

void Sanitizer::SetDefaultConfig() {
  MOZ_ASSERT(NS_IsMainThread());
  AssertNoLists();

  mIsDefaultConfig = true;

  // https://wicg.github.io/sanitizer-api/#built-in-safe-default-configuration
  // {
  //   ...
  //   "comments": false,
  //   "dataAttributes": false
  // }
  MOZ_ASSERT(!mComments);
  MOZ_ASSERT(!mDataAttributes);

  if (sDefaultHTMLElements) {
    // Already initialized.
    return;
  }

  sDefaultHTMLElements =
      new ElementsWithAttributes(std::size(kDefaultHTMLElements));
  size_t i = 0;
  for (nsStaticAtom* name : kDefaultHTMLElements) {
    UniquePtr<StaticAtomSet> attributes = nullptr;

    // Walkthrough the element specific attribute list in lockstep.
    // The last "name" in the array is a nullptr sentinel.
    if (name == kHTMLElementWithAttributes[i]) {
      attributes = MakeUnique<StaticAtomSet>();
      while (kHTMLElementWithAttributes[++i]) {
        attributes->Insert(kHTMLElementWithAttributes[i]);
      }
      i++;
    }

    sDefaultHTMLElements->InsertOrUpdate(name, std::move(attributes));
  }

  sDefaultMathMLElements =
      new ElementsWithAttributes(std::size(kDefaultMathMLElements));
  i = 0;
  for (nsStaticAtom* name : kDefaultMathMLElements) {
    UniquePtr<StaticAtomSet> attributes = nullptr;

    if (name == kMathMLElementWithAttributes[i]) {
      attributes = MakeUnique<StaticAtomSet>();
      while (kMathMLElementWithAttributes[++i]) {
        attributes->Insert(kMathMLElementWithAttributes[i]);
      }
      i++;
    }

    sDefaultMathMLElements->InsertOrUpdate(name, std::move(attributes));
  }

  sDefaultAttributes = new StaticAtomSet(std::size(kDefaultAttributes));
  for (nsStaticAtom* name : kDefaultAttributes) {
    sDefaultAttributes->Insert(name);
  }

  ClearOnShutdown(&sDefaultHTMLElements);
  ClearOnShutdown(&sDefaultMathMLElements);
  ClearOnShutdown(&sDefaultAttributes);
}

// https://wicg.github.io/sanitizer-api/#sanitizer-set-a-configuration
void Sanitizer::SetConfig(const SanitizerConfig& aConfig,
                          bool aAllowCommentsAndDataAttributes,
                          ErrorResult& aRv) {
  // Step 1. For each element of configuration["elements"] do:
  if (aConfig.mElements.WasPassed()) {
    for (const auto& element : aConfig.mElements.Value()) {
      // Step 1.1. Call allow an element with element and sanitizer’s
      // configuration.
      AllowElement(element);
    }
  }

  // Step 2. For each element of configuration["removeElements"] do:
  if (aConfig.mRemoveElements.WasPassed()) {
    for (const auto& element : aConfig.mRemoveElements.Value()) {
      // Step 2.1. Call remove an element with element and sanitizer’s
      // configuration.
      RemoveElement(element);
    }
  }

  // Step 3. For each element of configuration["replaceWithChildrenElements"]
  // do:
  if (aConfig.mReplaceWithChildrenElements.WasPassed()) {
    for (const auto& element : aConfig.mReplaceWithChildrenElements.Value()) {
      // Step 3.1. Call replace an element with its children with element and
      // sanitizer’s configuration.
      ReplaceElementWithChildren(element);
    }
  }

  // Step 4. For each attribute of configuration["attributes"] do:
  if (aConfig.mAttributes.WasPassed()) {
    for (const auto& attribute : aConfig.mAttributes.Value()) {
      // Step 4.1. Call allow an attribute with attribute and sanitizer’s
      // configuration.
      AllowAttribute(attribute);
    }
  }

  // Step 5. For each attribute of configuration["removeAttributes"] do:
  if (aConfig.mRemoveAttributes.WasPassed()) {
    for (const auto& attribute : aConfig.mRemoveAttributes.Value()) {
      // Step 5.1. Call remove an attribute with attribute and sanitizer’s
      // configuration.
      RemoveAttribute(attribute);
    }
  }

  // Step 6. If configuration["comments"] exists:
  if (aConfig.mComments.WasPassed()) {
    // Step 6.1. Then call set comments with configuration["comments"] and
    // sanitizer’s configuration.
    SetComments(aConfig.mComments.Value());
  } else {
    // Step 6.2. Otherwise call set comments with allowCommentsAndDataAttributes
    // and sanitizer’s configuration.
    SetComments(aAllowCommentsAndDataAttributes);
  }

  // Step 7. If configuration["dataAttributes"] exists:
  if (aConfig.mDataAttributes.WasPassed()) {
    // Step 7.1. Then call set data attributes with
    // configuration["dataAttributes"] and sanitizer’s configuration.
    SetDataAttributes(aConfig.mDataAttributes.Value());
  } else {
    // Step 7.2. Otherwise call set data attributes with
    // allowCommentsAndDataAttributes and sanitizer’s configuration.
    SetDataAttributes(aAllowCommentsAndDataAttributes);
  }

  // Step 8. Return whether all of the following are true:

  auto isSameSize = [](const auto& aInputConfig, const auto& aProcessedConfig) {
    size_t sizeInput =
        aInputConfig.WasPassed() ? aInputConfig.Value().Length() : 0;
    size_t sizeProcessed = aProcessedConfig.Values().Length();
    return sizeInput == sizeProcessed;
  };

  // TODO: Better error messages. (e.g. show difference before and after?)

  // size of configuration["elements"] equals size of sanitizer’s
  // configuration["elements"].
  if (!isSameSize(aConfig.mElements, mElements)) {
    aRv.ThrowTypeError("'elements' changed");
    return;
  }

  // size of configuration["removeElements"] equals size of sanitizer’s
  // configuration["removeElements"].
  if (!isSameSize(aConfig.mRemoveElements, mRemoveElements)) {
    aRv.ThrowTypeError("'removeElements' changed");
    return;
  }

  // size of configuration["replaceWithChildrenElements"] equals size of
  // sanitizer’s configuration["replaceWithChildrenElements"].
  if (!isSameSize(aConfig.mReplaceWithChildrenElements,
                  mReplaceWithChildrenElements)) {
    aRv.ThrowTypeError("'replaceWithChildrenElements' changed");
    return;
  }

  // size of configuration["attributes"] equals size of sanitizer’s
  // configuration["attributes"].
  if (!isSameSize(aConfig.mAttributes, mAttributes)) {
    aRv.ThrowTypeError("'attributes' changed");
    return;
  }

  // size of configuration["removeAttributes"] equals size of sanitizer’s
  // configuration["removeAttributes"].
  if (!isSameSize(aConfig.mRemoveAttributes, mRemoveAttributes)) {
    aRv.ThrowTypeError("'removeAttributes' changed");
    return;
  }

  // Either configuration["elements"] or configuration["removeElements"] exist,
  // or neither, but not both.
  if (aConfig.mElements.WasPassed() && aConfig.mRemoveElements.WasPassed()) {
    aRv.ThrowTypeError(
        "'elements' and 'removeElements' are not allowed at the same time");
    return;
  }

  // Either configuration["attributes"] or configuration["removeAttributes"]
  // exist, or neither, but not both.
  if (aConfig.mAttributes.WasPassed() &&
      aConfig.mRemoveAttributes.WasPassed()) {
    aRv.ThrowTypeError(
        "'attributes' and 'removeAttributes' are not allowed at the same time");
    return;
  }
}

// Turn the lazy default config into real lists that can be
// modified or queried via get().
void Sanitizer::MaybeMaterializeDefaultConfig() {
  if (!mIsDefaultConfig) {
    return;
  }
  mIsDefaultConfig = false;

  AssertNoLists();

  size_t i = 0;
  for (nsStaticAtom* name : kDefaultHTMLElements) {
    CanonicalElementWithAttributes element(
        CanonicalName(name, nsGkAtoms::nsuri_xhtml));

    if (name == kHTMLElementWithAttributes[i]) {
      ListSet<CanonicalName> attributes;
      while (kHTMLElementWithAttributes[++i]) {
        attributes.InsertNew(
            CanonicalName(kHTMLElementWithAttributes[i], nullptr));
      }
      i++;
      element.mAttributes = Some(std::move(attributes));
    }

    mElements.InsertNew(std::move(element));
  }

  i = 0;
  for (nsStaticAtom* name : kDefaultMathMLElements) {
    CanonicalElementWithAttributes element(
        CanonicalName(name, nsGkAtoms::nsuri_mathml));

    if (name == kMathMLElementWithAttributes[i]) {
      ListSet<CanonicalName> attributes;
      while (kMathMLElementWithAttributes[++i]) {
        attributes.InsertNew(
            CanonicalName(kMathMLElementWithAttributes[i], nullptr));
      }
      i++;
      element.mAttributes = Some(std::move(attributes));
    }

    mElements.InsertNew(std::move(element));
  }

  for (nsStaticAtom* name : kDefaultAttributes) {
    mAttributes.InsertNew(CanonicalName(name, nullptr));
  }
}

void Sanitizer::Get(SanitizerConfig& aConfig) {
  MaybeMaterializeDefaultConfig();

  nsTArray<OwningStringOrSanitizerElementNamespaceWithAttributes> elements;
  for (const CanonicalElementWithAttributes& canonical : mElements.Values()) {
    elements.AppendElement()->SetAsSanitizerElementNamespaceWithAttributes() =
        canonical.ToSanitizerElementNamespaceWithAttributes();
  }
  aConfig.mElements.Construct(std::move(elements));

  nsTArray<OwningStringOrSanitizerElementNamespace> removeElements;
  for (const CanonicalName& canonical : mRemoveElements.Values()) {
    removeElements.AppendElement()->SetAsSanitizerElementNamespace() =
        canonical.ToSanitizerElementNamespace();
  }
  aConfig.mRemoveElements.Construct(std::move(removeElements));

  nsTArray<OwningStringOrSanitizerElementNamespace> replaceWithChildrenElements;
  for (const CanonicalName& canonical : mReplaceWithChildrenElements.Values()) {
    replaceWithChildrenElements.AppendElement()
        ->SetAsSanitizerElementNamespace() =
        canonical.ToSanitizerElementNamespace();
  }
  aConfig.mReplaceWithChildrenElements.Construct(
      std::move(replaceWithChildrenElements));

  aConfig.mAttributes.Construct(ToSanitizerAttributes(mAttributes));
  aConfig.mRemoveAttributes.Construct(ToSanitizerAttributes(mRemoveAttributes));

  aConfig.mComments.Construct(mComments);
  aConfig.mDataAttributes.Construct(mDataAttributes);
}

auto& GetAsSanitizerElementNamespace(
    const StringOrSanitizerElementNamespace& aElement) {
  return aElement.GetAsSanitizerElementNamespace();
}
auto& GetAsSanitizerElementNamespace(
    const OwningStringOrSanitizerElementNamespace& aElement) {
  return aElement.GetAsSanitizerElementNamespace();
}
auto& GetAsSanitizerElementNamespace(
    const StringOrSanitizerElementNamespaceWithAttributes& aElement) {
  return aElement.GetAsSanitizerElementNamespaceWithAttributes();
}
auto& GetAsSanitizerElementNamespace(
    const OwningStringOrSanitizerElementNamespaceWithAttributes& aElement) {
  return aElement.GetAsSanitizerElementNamespaceWithAttributes();
}

// https://wicg.github.io/sanitizer-api/#canonicalize-a-sanitizer-element
template <typename SanitizerElement>
static CanonicalName CanonicalizeElement(const SanitizerElement& aElement) {
  // return the result of canonicalize a sanitizer name with element and the
  // HTML namespace as the default namespace.

  // https://wicg.github.io/sanitizer-api/#canonicalize-a-sanitizer-name
  // Step 1. Assert: name is either a DOMString or a dictionary. (implicit)

  // Step 2. If name is a DOMString, then return «[ "name" → name, "namespace" →
  // defaultNamespace]».
  if (aElement.IsString()) {
    RefPtr<nsAtom> nameAtom = NS_AtomizeMainThread(aElement.GetAsString());
    return CanonicalName(nameAtom, nsGkAtoms::nsuri_xhtml);
  }

  // Step 3. Assert: name is a dictionary and name["name"] exists.
  const auto& elem = GetAsSanitizerElementNamespace(aElement);
  MOZ_ASSERT(!elem.mName.IsVoid());

  RefPtr<nsAtom> namespaceAtom;
  // Step 4. Let namespace be name["namespace"] if it exists, otherwise
  // defaultNamespace.
  //
  // Note: "namespace" always exists due to the WebIDL default value.
  //
  // Step 5. If namespace is the empty string, then set it to null.
  // defaultNamespace.
  if (!elem.mNamespace.IsEmpty()) {
    namespaceAtom = NS_AtomizeMainThread(elem.mNamespace);
  }

  // Step 6. Return «[
  //  "name" → name["name"],
  //  "namespace" → namespace
  //  )
  // ]».
  RefPtr<nsAtom> nameAtom = NS_AtomizeMainThread(elem.mName);
  return CanonicalName(nameAtom, namespaceAtom);
}

// https://wicg.github.io/sanitizer-api/#canonicalize-a-sanitizer-attribute
template <typename SanitizerAttribute>
static CanonicalName CanonicalizeAttribute(
    const SanitizerAttribute& aAttribute) {
  // return the result of canonicalize a sanitizer name with attribute and null
  // as the default namespace.

  // https://wicg.github.io/sanitizer-api/#canonicalize-a-sanitizer-name
  // Step 1. Assert: name is either a DOMString or a dictionary. (implicit)

  // Step 2. If name is a DOMString, then return «[ "name" → name, "namespace" →
  // defaultNamespace]».
  if (aAttribute.IsString()) {
    RefPtr<nsAtom> nameAtom = NS_AtomizeMainThread(aAttribute.GetAsString());
    return CanonicalName(nameAtom, nullptr);
  }

  // Step 3. Assert: name is a dictionary and name["name"] exists.
  const auto& attr = aAttribute.GetAsSanitizerAttributeNamespace();
  MOZ_ASSERT(!attr.mName.IsVoid());

  RefPtr<nsAtom> namespaceAtom;
  // Step 4. Let namespace be name["namespace"] if it exists, otherwise
  // defaultNamespace.

  // Step 5. If namespace is the empty string, then set it to
  // null.
  if (!attr.mNamespace.IsEmpty()) {
    namespaceAtom = NS_AtomizeMainThread(attr.mNamespace);
  }

  // Step 6. Return «[
  //  "name" → name["name"],
  //  "namespace" → namespace,
  //  )
  // ]».
  RefPtr<nsAtom> nameAtom = NS_AtomizeMainThread(attr.mName);
  return CanonicalName(nameAtom, namespaceAtom);
}

// https://wicg.github.io/sanitizer-api/#canonicalize-a-sanitizer-element-with-attributes
template <typename SanitizerElementWithAttributes>
static CanonicalElementWithAttributes CanonicalizeElementWithAttributes(
    const SanitizerElementWithAttributes& aElement) {
  // Step 1. Let result be the result of canonicalize a sanitizer element with
  // element.
  CanonicalElementWithAttributes result =
      CanonicalElementWithAttributes(CanonicalizeElement(aElement));

  // Step 2. If element is a dictionary:
  if (aElement.IsSanitizerElementNamespaceWithAttributes()) {
    auto& elem = aElement.GetAsSanitizerElementNamespaceWithAttributes();

    // Step 2.1. For each attribute in element["attributes"]:
    if (elem.mAttributes.WasPassed()) {
      ListSet<CanonicalName> attributes;
      for (const auto& attribute : elem.mAttributes.Value()) {
        // Step 2.1.1. Add the result of canonicalize a sanitizer attribute with
        // attribute to result["attributes"].
        attributes.Insert(CanonicalizeAttribute(attribute));
      }
      result.mAttributes = Some(std::move(attributes));
    }

    // Step 2.2. For each attribute in element["removeAttributes"]:
    if (elem.mRemoveAttributes.WasPassed()) {
      ListSet<CanonicalName> attributes;
      for (const auto& attribute : elem.mRemoveAttributes.Value()) {
        // Step 2.2.1. Add the result of canonicalize a sanitizer attribute with
        // attribute to result["removeAttributes"].
        attributes.Insert(CanonicalizeAttribute(attribute));
      }
      result.mRemoveAttributes = Some(std::move(attributes));
    }
  }

  // Step 3. Return result.
  return result;
}

// https://wicg.github.io/sanitizer-api/#sanitizerconfig-allow-an-element
template <typename SanitizerElementWithAttributes>
void Sanitizer::AllowElement(const SanitizerElementWithAttributes& aElement) {
  MaybeMaterializeDefaultConfig();

  // Step 1. Set element to the result of canonicalize a sanitizer element with
  // attributes with element.
  CanonicalElementWithAttributes element =
      CanonicalizeElementWithAttributes(aElement);

  // Step 2. Remove element from configuration["elements"].
  mElements.Remove(element);

  // Step 4. Remove element from configuration["removeElements"].
  mRemoveElements.Remove(element);

  // Step 5. Remove element from configuration["replaceWithChildrenElements"].
  mReplaceWithChildrenElements.Remove(element);

  // Step 3. Append element to configuration["elements"].
  mElements.Insert(std::move(element));
}

template void Sanitizer::AllowElement(
    const StringOrSanitizerElementNamespaceWithAttributes&);

// https://wicg.github.io/sanitizer-api/#sanitizer-remove-an-element
template <typename SanitizerElement>
void Sanitizer::RemoveElement(const SanitizerElement& aElement) {
  MaybeMaterializeDefaultConfig();

  // Step 1. Set element to the result of canonicalize a sanitizer element with
  // element.
  CanonicalName element = CanonicalizeElement(aElement);

  RemoveElementCanonical(std::move(element));
}

void Sanitizer::RemoveElementCanonical(CanonicalName&& aElement) {
  // Step 3. Remove element from configuration["elements"] list.
  mElements.Remove(aElement);

  // Step 4. Remove element from configuration["replaceWithChildrenElements"].
  mReplaceWithChildrenElements.Remove(aElement);

  // Step 2. Add element to configuration["removeElements"].
  mRemoveElements.Insert(std::move(aElement));
}

template void Sanitizer::RemoveElement(
    const StringOrSanitizerElementNamespace&);

// https://wicg.github.io/sanitizer-api/#sanitizer-replace-an-element-with-its-children
template <typename SanitizerElement>
void Sanitizer::ReplaceElementWithChildren(const SanitizerElement& aElement) {
  MaybeMaterializeDefaultConfig();

  // Step 1. Set element to the result of canonicalize a sanitizer element with
  // element.
  CanonicalName element = CanonicalizeElement(aElement);

  // Step 3. Remove element from configuration["removeElements"].
  mRemoveElements.Remove(element);

  // Step 4. Remove element from configuration["elements"] list.
  mElements.Remove(element);

  // Step 2. Add element to configuration["replaceWithChildrenElements"].
  mReplaceWithChildrenElements.Insert(std::move(element));
}

template void Sanitizer::ReplaceElementWithChildren(
    const StringOrSanitizerElementNamespace&);

// https://wicg.github.io/sanitizer-api/#sanitizer-allow-an-attribute
template <typename SanitizerAttribute>
void Sanitizer::AllowAttribute(const SanitizerAttribute& aAttribute) {
  MaybeMaterializeDefaultConfig();

  // Step 1. Set attribute to the result of canonicalize a sanitizer attribute
  // with attribute.
  CanonicalName attribute = CanonicalizeAttribute(aAttribute);

  // Step 3. Remove attribute from configuration["removeAttributes"].
  mRemoveAttributes.Remove(attribute);

  // Step 2. Add attribute to configuration["attributes"].
  mAttributes.Insert(std::move(attribute));
}

template void Sanitizer::AllowAttribute(
    const StringOrSanitizerAttributeNamespace&);

// https://wicg.github.io/sanitizer-api/#sanitizer-remove-an-attribute
template <typename SanitizerAttribute>
void Sanitizer::RemoveAttribute(const SanitizerAttribute& aAttribute) {
  MaybeMaterializeDefaultConfig();

  // Step 1. Set attribute to the result of canonicalize a sanitizer attribute
  // with attribute.
  CanonicalName attribute = CanonicalizeAttribute(aAttribute);

  RemoveAttributeCanonical(std::move(attribute));
}

void Sanitizer::RemoveAttributeCanonical(CanonicalName&& aAttribute) {
  // Step 3. Remove attribute from configuration["attributes"].
  mAttributes.Remove(aAttribute);

  // Step 2. Add attribute to configuration["removeAttributes"].
  mRemoveAttributes.Insert(std::move(aAttribute));
}

template void Sanitizer::RemoveAttribute(
    const StringOrSanitizerAttributeNamespace&);

void Sanitizer::SetComments(bool aAllow) {
  // The sanitize algorithm optimized for the default config supports
  // comments both being allowed and disallowed.
  mComments = aAllow;
}

void Sanitizer::SetDataAttributes(bool aAllow) {
  // Same as above for data-attributes.
  mDataAttributes = aAllow;
}

// https://wicg.github.io/sanitizer-api/#sanitizer-removeunsafe
void Sanitizer::RemoveUnsafe() {
  MaybeMaterializeDefaultConfig();

  // https://wicg.github.io/sanitizer-api/#sanitizerconfig-remove-unsafe
  // Step 1. Assert: (Implicit)
  // Step 2. Let result be a copy of configuration. (Unobservable)

  // Step 3. For each element in built-in safe baseline
  // configuration[removeElements]:
  //
  // Keep in sync with IsUnsafeElement
  RemoveElementCanonical(
      CanonicalName(nsGkAtoms::script, nsGkAtoms::nsuri_xhtml));
  RemoveElementCanonical(
      CanonicalName(nsGkAtoms::frame, nsGkAtoms::nsuri_xhtml));
  RemoveElementCanonical(
      CanonicalName(nsGkAtoms::iframe, nsGkAtoms::nsuri_xhtml));
  RemoveElementCanonical(
      CanonicalName(nsGkAtoms::object, nsGkAtoms::nsuri_xhtml));
  RemoveElementCanonical(
      CanonicalName(nsGkAtoms::embed, nsGkAtoms::nsuri_xhtml));
  RemoveElementCanonical(
      CanonicalName(nsGkAtoms::script, nsGkAtoms::nsuri_svg));
  RemoveElementCanonical(CanonicalName(nsGkAtoms::use, nsGkAtoms::nsuri_svg));

  // Step 4. For each attribute in built-in safe baseline
  // configuration[removeAttributes]: (Empty list)

  // Step 5. For each attribute listed in event handler content attributes:
  // TODO: Consider sorting these.
  nsContentUtils::ForEachEventAttributeName(
      EventNameType_All & ~EventNameType_XUL,
      [self = MOZ_KnownLive(this)](nsAtom* aName) {
        self->RemoveAttributeCanonical(CanonicalName(aName, nullptr));
      });

  // Step 6. Return result. (Overwrites "this’s configuration")
}

// https://wicg.github.io/sanitizer-api/#sanitize
void Sanitizer::Sanitize(nsINode* aNode, bool aSafe, ErrorResult& aRv) {
  MOZ_ASSERT(aNode->OwnerDoc()->IsLoadedAsData(),
             "SanitizeChildren relies on the document being inert to be safe");

  // Step 1. Let configuration be the value of sanitizer’s configuration.

  // Step 2. If safe is true, then set configuration to the result of calling
  // remove unsafe on configuration.
  //
  // Optimization: We really don't want to make a copy of the configuration
  // here, so we instead explictly remove the handful elements and
  // attributes that are part of "remove unsafe" in the
  // SanitizeChildren() and SanitizeAttributes() methods.

  // Step 3. Call sanitize core on node, configuration, and with
  // handleJavascriptNavigationUrls set to safe.
  if (mIsDefaultConfig) {
    AssertNoLists();
    SanitizeChildren<true>(aNode, aSafe);
  } else {
    SanitizeChildren<false>(aNode, aSafe);
  }
}

static RefPtr<nsAtom> ToNamespace(int32_t aNamespaceID) {
  if (aNamespaceID == kNameSpaceID_None) {
    return nullptr;
  }

  RefPtr<nsAtom> atom =
      nsNameSpaceManager::GetInstance()->NameSpaceURIAtom(aNamespaceID);
  return atom;
}

// https://wicg.github.io/sanitizer-api/#built-in-safe-baseline-configuration
// The "removeElements" list.
// Keep in sync with Sanitizer::RemoveUnsafe
static bool IsUnsafeElement(nsAtom* aLocalName, int32_t aNamespaceID) {
  if (aNamespaceID == kNameSpaceID_XHTML) {
    return aLocalName == nsGkAtoms::script || aLocalName == nsGkAtoms::frame ||
           aLocalName == nsGkAtoms::iframe || aLocalName == nsGkAtoms::object ||
           aLocalName == nsGkAtoms::embed;
  }
  if (aNamespaceID == kNameSpaceID_SVG) {
    return aLocalName == nsGkAtoms::script || aLocalName == nsGkAtoms::use;
  }
  return false;
}

// https://wicg.github.io/sanitizer-api/#sanitize-core
template <bool IsDefaultConfig>
void Sanitizer::SanitizeChildren(nsINode* aNode, bool aSafe) {
  // Step 1. For each child in current’s children:
  nsCOMPtr<nsIContent> next = nullptr;
  for (nsCOMPtr<nsIContent> child = aNode->GetFirstChild(); child;
       child = next) {
    next = child->GetNextSibling();

    // Step 1.1. Assert: child implements Text, Comment, Element, or
    // DocumentType.
    MOZ_ASSERT(child->IsText() || child->IsComment() || child->IsElement() ||
               child->NodeType() == nsINode::DOCUMENT_TYPE_NODE);

    // Step 1.2. If child implements DocumentType, then continue.
    if (child->NodeType() == nsINode::DOCUMENT_TYPE_NODE) {
      continue;
    }

    // Step 1.3. If child implements Text, then continue.
    if (child->IsText()) {
      continue;
    }

    // Step 1.4. If child implements Comment:
    if (child->IsComment()) {
      // Step 1.4.1 If configuration["comments"] is not true, then remove child.
      if (!mComments) {
        child->RemoveFromParent();
      }
      continue;
    }

    // Step 1.5. Otherwise:
    MOZ_ASSERT(child->IsElement());

    // Step 1.5.1. Let elementName be a SanitizerElementNamespace with child’s
    // local name and namespace.
    nsAtom* nameAtom = child->NodeInfo()->NameAtom();
    int32_t namespaceID = child->NodeInfo()->NamespaceID();
    // Make sure this is optimized away for the default config.
    Maybe<CanonicalName> elementName;
    if constexpr (!IsDefaultConfig) {
      elementName.emplace(nameAtom, ToNamespace(namespaceID));
    }

    // Optimization: Remove unsafe elements before doing anything else.
    // https://wicg.github.io/sanitizer-api/#built-in-safe-baseline-configuration
    //
    // We have to do this _before_ handling the "replaceWithChildrenElements"
    // list, because by adding the unsafe elements to the "removeElements" list
    // they would be implicitly deleted from the former.
    //
    // The default config's "elements" allow list does not contain any unsafe
    // elements so we can skip this.
    if constexpr (!IsDefaultConfig) {
      if (aSafe && IsUnsafeElement(nameAtom, namespaceID)) {
        child->RemoveFromParent();
        continue;
      }
    }

    // Step 1.5.2. If configuration["replaceWithChildrenElements"] contains
    // elementName:
    if constexpr (!IsDefaultConfig) {
      if (mReplaceWithChildrenElements.Contains(*elementName)) {
        // Note: This follows nsTreeSanitizer by first inserting the
        // child's children in place of the current child and then
        // continueing the sanitization from the first inserted grandchild.
        nsCOMPtr<nsIContent> parent = child->GetParent();
        nsCOMPtr<nsIContent> firstChild = child->GetFirstChild();
        nsCOMPtr<nsIContent> newChild = firstChild;
        for (; newChild; newChild = child->GetFirstChild()) {
          ErrorResult rv;
          parent->InsertBefore(*newChild, child, rv);
          if (rv.Failed()) {
            // TODO: Abort?
            break;
          }
        }

        child->RemoveFromParent();
        if (firstChild) {
          next = firstChild;
        }
        continue;
      }
    }

    // Step 1.5.3. If configuration["removeElements"] contains elementName, or
    // if configuration["elements"] is not empty and does not contain
    // elementName:
    [[maybe_unused]] StaticAtomSet* elementAttributes = nullptr;
    if constexpr (!IsDefaultConfig) {
      if (mRemoveElements.Contains(*elementName) ||
          (!mElements.IsEmpty() && !mElements.Contains(*elementName))) {
        // Step 1.5.3.1. Remove child.
        child->RemoveFromParent();
        // Step 1.5.3.2. Continue.
        continue;
      }
    } else {
      bool found = false;
      if (nameAtom->IsStatic()) {
        ElementsWithAttributes* elements = nullptr;
        if (namespaceID == kNameSpaceID_XHTML) {
          elements = sDefaultHTMLElements;
        } else if (namespaceID == kNameSpaceID_MathML) {
          elements = sDefaultMathMLElements;
        }
        if (elements) {
          if (auto lookup = elements->Lookup(nameAtom->AsStatic())) {
            found = true;
            // This is the nullptr for elements without specific allowed
            // attributes.
            elementAttributes = lookup->get();
          }
        }
      }
      if (!found) {
        // Step 1.5.3.1. Remove child.
        child->RemoveFromParent();
        // Step 1.5.3.2. Continue.
        continue;
      }
      MOZ_ASSERT(!IsUnsafeElement(nameAtom, namespaceID));
    }

    // Step 1.5.4. If elementName equals «[ "name" → "template", "namespace" →
    // HTML namespace ]»
    if (auto* templateEl = HTMLTemplateElement::FromNode(child)) {
      // Step 1.5.4.1. Then call sanitize core on child’s template contents with
      // configuration and handleJavascriptNavigationUrls.
      RefPtr<DocumentFragment> frag = templateEl->Content();
      SanitizeChildren<IsDefaultConfig>(frag, aSafe);
    }

    // Step 1.5.5. If child is a shadow host, then call sanitize core on child’s
    // shadow root with configuration and handleJavascriptNavigationUrls.
    if (RefPtr<ShadowRoot> shadow = child->GetShadowRoot()) {
      SanitizeChildren<IsDefaultConfig>(shadow, aSafe);
    }

    // Step 1.5.6.
    if constexpr (!IsDefaultConfig) {
      SanitizeAttributes(child->AsElement(), *elementName, aSafe);
    } else {
      SanitizeDefaultConfigAttributes(child->AsElement(), elementAttributes,
                                      aSafe);
    }

    // Step 1.5.7. Call sanitize core on child with configuration and
    // handleJavascriptNavigationUrls.
    // TODO: Optimization: Remove recusion similar to nsTreeSanitizer
    SanitizeChildren<IsDefaultConfig>(child, aSafe);
  }
}

static inline bool IsDataAttribute(nsAtom* aName, int32_t aNamespaceID) {
  return StringBeginsWith(nsDependentAtomString(aName), u"data-"_ns) &&
         aNamespaceID == kNameSpaceID_None;
}

// https://wicg.github.io/sanitizer-api/#sanitize-core
// Step 2.4.6.5. If handleJavascriptNavigationUrls:
static bool RemoveJavascriptNavigationURLAttribute(Element* aElement,
                                                   nsAtom* aLocalName,
                                                   int32_t aNamespaceID) {
  auto containsJavascriptURL = [&]() {
    nsAutoString value;
    if (!aElement->GetAttr(aNamespaceID, aLocalName, value)) {
      return false;
    }

    // https://wicg.github.io/sanitizer-api/#contains-a-javascript-url
    // Step 1. Let url be the result of running the basic URL parser on
    // attribute’s value.
    // XXX follow base-uri?
    nsCOMPtr<nsIURI> uri;
    if (NS_FAILED(NS_NewURI(getter_AddRefs(uri), value))) {
      // Step 2. If url is failure, then return false.
      return false;
    }

    // Step 3. Return whether url’s scheme is "javascript".
    return uri->SchemeIs("javascript");
  };

  // Step 1. If «[elementName, attrName]» matches an entry in the built-in
  // navigating URL attributes list, and if attribute contains a javascript:
  // URL, then remove attribute from child.
  if ((aElement->IsAnyOfHTMLElements(nsGkAtoms::a, nsGkAtoms::area,
                                     nsGkAtoms::base) &&
       aLocalName == nsGkAtoms::href && aNamespaceID == kNameSpaceID_None) ||
      (aElement->IsAnyOfHTMLElements(nsGkAtoms::button, nsGkAtoms::input) &&
       aLocalName == nsGkAtoms::formaction &&
       aNamespaceID == kNameSpaceID_None) ||
      (aElement->IsHTMLElement(nsGkAtoms::form) &&
       aLocalName == nsGkAtoms::action && aNamespaceID == kNameSpaceID_None) ||
      (aElement->IsHTMLElement(nsGkAtoms::iframe) &&
       aLocalName == nsGkAtoms::src && aNamespaceID == kNameSpaceID_None) ||
      (aElement->IsSVGElement(nsGkAtoms::a) && aLocalName == nsGkAtoms::href &&
       (aNamespaceID == kNameSpaceID_None ||
        aNamespaceID == kNameSpaceID_XLink))) {
    if (containsJavascriptURL()) {
      return true;
    }
  };

  // Step 2. If child’s namespace is the MathML Namespace and attr’s local name
  // is "href" and attr’s namespace is null or the XLink namespace and attr
  // contains a javascript: URL, then remove attr.
  if (aElement->IsMathMLElement() && aLocalName == nsGkAtoms::href &&
      (aNamespaceID == kNameSpaceID_None ||
       aNamespaceID == kNameSpaceID_XLink)) {
    if (containsJavascriptURL()) {
      return true;
    }
  }

  // Step 3. If the built-in animating URL attributes list contains
  // «[elementName, attrName]» and attr’s value is "href" or "xlink:href", then
  // remove attr.
  if (aLocalName == nsGkAtoms::attributeName &&
      aNamespaceID == kNameSpaceID_None &&
      aElement->IsAnyOfSVGElements(nsGkAtoms::animate, nsGkAtoms::animateMotion,
                                   nsGkAtoms::animateTransform,
                                   nsGkAtoms::set)) {
    nsAutoString value;
    if (!aElement->GetAttr(aNamespaceID, aLocalName, value)) {
      return false;
    }

    return value.EqualsLiteral("href") || value.EqualsLiteral("xlink:href");
  }

  return false;
}

void Sanitizer::SanitizeAttributes(Element* aChild,
                                   const CanonicalName& aElementName,
                                   bool aSafe) {
  MOZ_ASSERT(!mIsDefaultConfig);

  // TODO: Replace this with a hashmap.
  const CanonicalElementWithAttributes* elementWithAttributes =
      mElements.Get(aElementName);

  // https://wicg.github.io/sanitizer-api/#sanitize-core
  // Substeps of
  //  Step 1.5.6. For each attribute in child’s attribute list:
  int32_t count = int32_t(aChild->GetAttrCount());
  for (int32_t i = count - 1; i >= 0; --i) {
    // Step 1. Let attrName be a SanitizerAttributeNamespace with attribute’s
    // local name and namespace.
    const nsAttrName* attr = aChild->GetAttrNameAt(i);
    RefPtr<nsAtom> attrLocalName = attr->LocalName();
    int32_t attrNs = attr->NamespaceID();
    CanonicalName attrName(attrLocalName, ToNamespace(attrNs));

    bool remove = false;
    // Optimization: Remove unsafe event handler content attributes.
    // https://wicg.github.io/sanitizer-api/#sanitizerconfig-remove-unsafe
    if (aSafe && attrNs == kNameSpaceID_None &&
        nsContentUtils::IsEventAttributeName(
            attrLocalName, EventNameType_All & ~EventNameType_XUL)) {
      remove = true;
    }

    // Step 2. If configuration["removeAttributes"] contains attrName, then
    // Remove attribute from child.
    else if (mRemoveAttributes.Contains(attrName)) {
      remove = true;
    }

    // Step 3. If configuration["elements"]["removeAttributes"] contains
    // attrName, then remove attribute from child.
    // XXX:
    //  Spec issue configuration["elements"][elementName]["removeAttributes"] ??
    else if (elementWithAttributes &&
             elementWithAttributes->mRemoveAttributes &&
             elementWithAttributes->mRemoveAttributes->Contains(attrName)) {
      remove = true;
    }

    // Step 4. If all of the following are false, then remove attribute from
    // child.
    // - configuration["attributes"] exists and contains attrName
    //    TODO: exists check
    // - configuration["elements"]["attributes"] contains attrName
    // - "data-" is a code unit prefix of local name and namespace is null and
    // configuration["dataAttributes"] is true
    else if ((!mAttributes.IsEmpty() && !mAttributes.Contains(attrName)) &&
             !(elementWithAttributes && elementWithAttributes->mAttributes &&
               elementWithAttributes->mAttributes->Contains(attrName)) &&
             !(mDataAttributes && IsDataAttribute(attrLocalName, attrNs))) {
      remove = true;
    }

    // Step 5. If handleJavascriptNavigationUrls:
    else if (aSafe) {
      remove =
          RemoveJavascriptNavigationURLAttribute(aChild, attrLocalName, attrNs);
    }

    if (remove) {
      aChild->UnsetAttr(attr->NamespaceID(), attr->LocalName(), false);

      // XXX Copied from nsTreeSanitizer.
      // In case the attribute removal shuffled the attribute order, start the
      // loop again.
      --count;
      i = count;  // i will be decremented immediately thanks to the for loop
    }
  }
}

void Sanitizer::SanitizeDefaultConfigAttributes(
    Element* aChild, StaticAtomSet* aElementAttributes, bool aSafe) {
  MOZ_ASSERT(mIsDefaultConfig);

  // https://wicg.github.io/sanitizer-api/#sanitize-core
  // Substeps of
  //  Step 1.5.6. For each attribute in child’s attribute list:
  int32_t count = int32_t(aChild->GetAttrCount());
  for (int32_t i = count - 1; i >= 0; --i) {
    // Step 1. Let attrName be a SanitizerAttributeNamespace with attribute’s
    // local name and namespace.
    const nsAttrName* attr = aChild->GetAttrNameAt(i);
    RefPtr<nsAtom> attrLocalName = attr->LocalName();
    int32_t attrNs = attr->NamespaceID();

    // Step 2. If configuration["removeAttributes"] contains attrName, then
    // Remove attribute from child.
    // Step 3. If configuration["elements"]["removeAttributes"] contains
    // attrName, then remove attribute from child.
    //
    // Note: Empty/missing for the default config.

    // Step 4. If all of the following are false, then remove attribute from
    // child.
    // - configuration["attributes"] exists and contains attrName
    // - configuration["elements"]["attributes"] contains attrName
    // - "data-" is a code unit prefix of local name and namespace is null and
    // configuration["dataAttributes"] is true
    bool remove = false;
    // Note: All attributes allowed by the default config are in the "null"
    // namespace.
    if (attrNs != kNameSpaceID_None ||
        (!sDefaultAttributes->Contains(attrLocalName) &&
         !(aElementAttributes && aElementAttributes->Contains(attrLocalName)) &&
         !(mDataAttributes && IsDataAttribute(attrLocalName, attrNs)))) {
      remove = true;
    }

    // Step 5. If handleJavascriptNavigationUrls:
    else if (aSafe) {
      // TODO: This can be further optimized, because the default config
      // at the moment only allows <a href>.
      remove =
          RemoveJavascriptNavigationURLAttribute(aChild, attrLocalName, attrNs);
    }

    // The default config attribute allow lists don't contain event
    // handler attributes.
    MOZ_ASSERT_IF(!remove,
                  !nsContentUtils::IsEventAttributeName(
                      attrLocalName, EventNameType_All & ~EventNameType_XUL));

    if (remove) {
      aChild->UnsetAttr(attr->NamespaceID(), attr->LocalName(), false);

      // XXX Copied from nsTreeSanitizer.
      // In case the attribute removal shuffled the attribute order, start the
      // loop again.
      --count;
      i = count;  // i will be decremented immediately thanks to the for loop
    }
  }
}

/* ------ Logging ------ */

void Sanitizer::LogLocalizedString(const char* aName,
                                   const nsTArray<nsString>& aParams,
                                   uint32_t aFlags) {
  uint64_t innerWindowID = 0;
  bool isPrivateBrowsing = true;
  nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(mGlobal);
  if (window && window->GetDoc()) {
    auto* doc = window->GetDoc();
    innerWindowID = doc->InnerWindowID();
    isPrivateBrowsing = doc->IsInPrivateBrowsing();
  }
  nsAutoString logMsg;
  nsContentUtils::FormatLocalizedString(nsContentUtils::eSECURITY_PROPERTIES,
                                        aName, aParams, logMsg);
  LogMessage(logMsg, aFlags, innerWindowID, isPrivateBrowsing);
}

/* static */
void Sanitizer::LogMessage(const nsAString& aMessage, uint32_t aFlags,
                           uint64_t aInnerWindowID, bool aFromPrivateWindow) {
  // Prepending 'Sanitizer' to the outgoing console message
  nsString message;
  message.AppendLiteral(u"Sanitizer: ");
  message.Append(aMessage);

  // Allow for easy distinction in devtools code.
  constexpr auto category = "Sanitizer"_ns;

  if (aInnerWindowID > 0) {
    // Send to content console
    nsContentUtils::ReportToConsoleByWindowID(message, aFlags, category,
                                              aInnerWindowID);
  } else {
    // Send to browser console
    nsContentUtils::LogSimpleConsoleError(message, category, aFromPrivateWindow,
                                          true /* from chrome context */,
                                          aFlags);
  }
}

}  // namespace mozilla::dom
