/* -*- 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 "js/Array.h"  // JS::GetArrayLength, JS::IsArrayObject
#include "js/JSON.h"
#include "js/Object.h"
#include "js/PropertyAndElement.h"  // JS_GetElement
#include "js/TypeDecls.h"
#include "jsapi.h"
#include "mozilla/BasePrincipal.h"
#include "mozilla/ErrorResult.h"
#include "mozilla/PresShell.h"
#include "mozilla/dom/AutocompleteInfoBinding.h"
#include "mozilla/dom/CanonicalBrowsingContext.h"
#include "mozilla/dom/CustomElementTypes.h"
#include "mozilla/dom/CustomElementRegistry.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/DocumentInlines.h"
#include "mozilla/dom/FormData.h"
#include "mozilla/dom/FragmentOrElement.h"
#include "mozilla/dom/HTMLElement.h"
#include "mozilla/dom/HTMLInputElement.h"
#include "mozilla/dom/HTMLSelectElement.h"
#include "mozilla/dom/HTMLTextAreaElement.h"
#include "mozilla/dom/RootedDictionary.h"
#include "mozilla/dom/SessionStorageManager.h"
#include "mozilla/dom/PBackgroundSessionStorageCache.h"
#include "mozilla/dom/SessionStoreChangeListener.h"
#include "mozilla/dom/SessionStoreChild.h"
#include "mozilla/dom/SessionStoreUtils.h"
#include "mozilla/dom/SessionStoreUtilsBinding.h"
#include "mozilla/dom/ToJSValue.h"
#include "mozilla/dom/UnionTypes.h"
#include "mozilla/dom/sessionstore/SessionStoreTypes.h"
#include "mozilla/dom/txIXPathContext.h"
#include "mozilla/dom/WindowGlobalParent.h"
#include "mozilla/dom/WindowProxyHolder.h"
#include "mozilla/dom/XPathResult.h"
#include "mozilla/dom/XPathEvaluator.h"
#include "mozilla/dom/XPathExpression.h"
#include "mozilla/dom/PBackgroundSessionStorageCache.h"
#include "mozilla/ipc/BackgroundUtils.h"
#include "mozilla/ReverseIterator.h"
#include "mozilla/UniquePtr.h"
#include "nsCharSeparatedTokenizer.h"
#include "nsContentList.h"
#include "nsContentUtils.h"
#include "nsFocusManager.h"
#include "nsGlobalWindowOuter.h"
#include "nsIContentInlines.h"
#include "nsIDocShell.h"
#include "nsIFormControl.h"
#include "nsIScrollableFrame.h"
#include "nsISHistory.h"
#include "nsIXULRuntime.h"
#include "nsPresContext.h"
#include "nsPrintfCString.h"

using namespace mozilla;
using namespace mozilla::dom;
using namespace mozilla::dom::sessionstore;

namespace {

class DynamicFrameEventFilter final : public nsIDOMEventListener {
 public:
  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
  NS_DECL_CYCLE_COLLECTION_CLASS(DynamicFrameEventFilter)

  explicit DynamicFrameEventFilter(EventListener* aListener)
      : mListener(aListener) {}

  NS_IMETHODIMP HandleEvent(Event* aEvent) override {
    if (mListener && TargetInNonDynamicDocShell(aEvent)) {
      mListener->HandleEvent(*aEvent);
    }

    return NS_OK;
  }

 private:
  ~DynamicFrameEventFilter() = default;

  bool TargetInNonDynamicDocShell(Event* aEvent) {
    EventTarget* target = aEvent->GetTarget();
    if (!target) {
      return false;
    }

    nsPIDOMWindowOuter* outer = target->GetOwnerGlobalForBindingsInternal();
    if (!outer || !outer->GetDocShell()) {
      return false;
    }

    RefPtr<BrowsingContext> context = outer->GetBrowsingContext();
    return context && !context->CreatedDynamically();
  }

  RefPtr<EventListener> mListener;
};

NS_IMPL_CYCLE_COLLECTION(DynamicFrameEventFilter, mListener)

NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DynamicFrameEventFilter)
  NS_INTERFACE_MAP_ENTRY(nsISupports)
  NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener)
NS_INTERFACE_MAP_END

NS_IMPL_CYCLE_COLLECTING_ADDREF(DynamicFrameEventFilter)
NS_IMPL_CYCLE_COLLECTING_RELEASE(DynamicFrameEventFilter)

}  // anonymous namespace

/* static */
void SessionStoreUtils::ForEachNonDynamicChildFrame(
    const GlobalObject& aGlobal, WindowProxyHolder& aWindow,
    SessionStoreUtilsFrameCallback& aCallback, ErrorResult& aRv) {
  if (!aWindow.get()) {
    aRv.Throw(NS_ERROR_INVALID_ARG);
    return;
  }

  nsCOMPtr<nsIDocShell> docShell = aWindow.get()->GetDocShell();
  if (!docShell) {
    aRv.Throw(NS_ERROR_FAILURE);
    return;
  }

  int32_t length;
  aRv = docShell->GetInProcessChildCount(&length);
  if (aRv.Failed()) {
    return;
  }

  for (int32_t i = 0; i < length; ++i) {
    nsCOMPtr<nsIDocShellTreeItem> item;
    docShell->GetInProcessChildAt(i, getter_AddRefs(item));
    if (!item) {
      aRv.Throw(NS_ERROR_FAILURE);
      return;
    }

    RefPtr<BrowsingContext> context = item->GetBrowsingContext();
    if (!context) {
      aRv.Throw(NS_ERROR_FAILURE);
      return;
    }

    if (!context->CreatedDynamically()) {
      int32_t childOffset = context->ChildOffset();
      aCallback.Call(WindowProxyHolder(context.forget()), childOffset);
    }
  }
}

/* static */
already_AddRefed<nsISupports>
SessionStoreUtils::AddDynamicFrameFilteredListener(
    const GlobalObject& aGlobal, EventTarget& aTarget, const nsAString& aType,
    JS::Handle<JS::Value> aListener, bool aUseCapture, bool aMozSystemGroup,
    ErrorResult& aRv) {
  if (NS_WARN_IF(!aListener.isObject())) {
    aRv.Throw(NS_ERROR_INVALID_ARG);
    return nullptr;
  }

  JSContext* cx = aGlobal.Context();
  JS::Rooted<JSObject*> obj(cx, &aListener.toObject());
  JS::Rooted<JSObject*> global(cx, JS::CurrentGlobalOrNull(cx));
  RefPtr<EventListener> listener =
      new EventListener(cx, obj, global, GetIncumbentGlobal());

  nsCOMPtr<nsIDOMEventListener> filter(new DynamicFrameEventFilter(listener));
  if (aMozSystemGroup) {
    aRv = aTarget.AddSystemEventListener(aType, filter, aUseCapture);
  } else {
    aRv = aTarget.AddEventListener(aType, filter, aUseCapture);
  }
  if (aRv.Failed()) {
    return nullptr;
  }

  return filter.forget();
}

/* static */
void SessionStoreUtils::RemoveDynamicFrameFilteredListener(
    const GlobalObject& global, EventTarget& aTarget, const nsAString& aType,
    nsISupports* aListener, bool aUseCapture, bool aMozSystemGroup,
    ErrorResult& aRv) {
  nsCOMPtr<nsIDOMEventListener> listener = do_QueryInterface(aListener);
  if (!listener) {
    aRv.Throw(NS_ERROR_NO_INTERFACE);
    return;
  }

  if (aMozSystemGroup) {
    aTarget.RemoveSystemEventListener(aType, listener, aUseCapture);
  } else {
    aTarget.RemoveEventListener(aType, listener, aUseCapture);
  }
}

/* static */
void SessionStoreUtils::CollectDocShellCapabilities(const GlobalObject& aGlobal,
                                                    nsIDocShell* aDocShell,
                                                    nsCString& aRetVal) {
  bool allow;

#define TRY_ALLOWPROP(y)          \
  PR_BEGIN_MACRO                  \
  aDocShell->GetAllow##y(&allow); \
  if (!allow) {                   \
    if (!aRetVal.IsEmpty()) {     \
      aRetVal.Append(',');        \
    }                             \
    aRetVal.Append(#y);           \
  }                               \
  PR_END_MACRO

  TRY_ALLOWPROP(Plugins);
  // Bug 1328013 : Don't collect "AllowJavascript" property
  // TRY_ALLOWPROP(Javascript);
  TRY_ALLOWPROP(MetaRedirects);
  TRY_ALLOWPROP(Subframes);
  TRY_ALLOWPROP(Images);
  TRY_ALLOWPROP(Media);
  TRY_ALLOWPROP(DNSPrefetch);
  TRY_ALLOWPROP(WindowControl);
  TRY_ALLOWPROP(Auth);
  TRY_ALLOWPROP(ContentRetargeting);
  TRY_ALLOWPROP(ContentRetargetingOnChildren);
#undef TRY_ALLOWPROP
}

/* static */
void SessionStoreUtils::RestoreDocShellCapabilities(
    nsIDocShell* aDocShell, const nsCString& aDisallowCapabilities) {
  aDocShell->SetAllowPlugins(true);
  aDocShell->SetAllowMetaRedirects(true);
  aDocShell->SetAllowSubframes(true);
  aDocShell->SetAllowImages(true);
  aDocShell->SetAllowMedia(true);
  aDocShell->SetAllowDNSPrefetch(true);
  aDocShell->SetAllowWindowControl(true);
  aDocShell->SetAllowContentRetargeting(true);
  aDocShell->SetAllowContentRetargetingOnChildren(true);

  bool allowJavascript = true;
  for (const nsACString& token :
       nsCCharSeparatedTokenizer(aDisallowCapabilities, ',').ToRange()) {
    if (token.EqualsLiteral("Plugins")) {
      aDocShell->SetAllowPlugins(false);
    } else if (token.EqualsLiteral("Javascript")) {
      allowJavascript = false;
    } else if (token.EqualsLiteral("MetaRedirects")) {
      aDocShell->SetAllowMetaRedirects(false);
    } else if (token.EqualsLiteral("Subframes")) {
      aDocShell->SetAllowSubframes(false);
    } else if (token.EqualsLiteral("Images")) {
      aDocShell->SetAllowImages(false);
    } else if (token.EqualsLiteral("Media")) {
      aDocShell->SetAllowMedia(false);
    } else if (token.EqualsLiteral("DNSPrefetch")) {
      aDocShell->SetAllowDNSPrefetch(false);
    } else if (token.EqualsLiteral("WindowControl")) {
      aDocShell->SetAllowWindowControl(false);
    } else if (token.EqualsLiteral("ContentRetargeting")) {
      bool allow;
      aDocShell->GetAllowContentRetargetingOnChildren(&allow);
      aDocShell->SetAllowContentRetargeting(
          false);  // will also set AllowContentRetargetingOnChildren
      aDocShell->SetAllowContentRetargetingOnChildren(
          allow);  // restore the allowProp to original
    } else if (token.EqualsLiteral("ContentRetargetingOnChildren")) {
      aDocShell->SetAllowContentRetargetingOnChildren(false);
    }
  }

  if (!mozilla::SessionHistoryInParent()) {
    // With SessionHistoryInParent, this is set from the parent process.
    BrowsingContext* bc = aDocShell->GetBrowsingContext();
    Unused << bc->SetAllowJavascript(allowJavascript);
  }
}

static void CollectCurrentScrollPosition(JSContext* aCx, Document& aDocument,
                                         Nullable<CollectedData>& aRetVal) {
  PresShell* presShell = aDocument.GetPresShell();
  if (!presShell) {
    return;
  }
  nsPoint scrollPos = presShell->GetVisualViewportOffset();
  int scrollX = nsPresContext::AppUnitsToIntCSSPixels(scrollPos.x);
  int scrollY = nsPresContext::AppUnitsToIntCSSPixels(scrollPos.y);

  if ((scrollX != 0) || (scrollY != 0)) {
    aRetVal.SetValue().mScroll.Construct() =
        nsPrintfCString("%d,%d", scrollX, scrollY);
  }
}

/* static */
void SessionStoreUtils::RestoreScrollPosition(const GlobalObject& aGlobal,
                                              nsGlobalWindowInner& aWindow,
                                              const CollectedData& aData) {
  if (aData.mScroll.WasPassed()) {
    RestoreScrollPosition(aWindow, aData.mScroll.Value());
  }
}

/* static */
void SessionStoreUtils::RestoreScrollPosition(
    nsGlobalWindowInner& aWindow, const nsCString& aScrollPosition) {
  using Change = mozilla::dom::SessionStoreChangeListener::Change;
  SessionStoreChangeListener::CollectSessionStoreData(
      aWindow.GetWindowContext(), EnumSet<Change>(Change::Scroll));

  nsCCharSeparatedTokenizer tokenizer(aScrollPosition, ',');
  nsAutoCString token(tokenizer.nextToken());
  int pos_X = atoi(token.get());
  token = tokenizer.nextToken();
  int pos_Y = atoi(token.get());

  aWindow.ScrollTo(pos_X, pos_Y);

  if (nsCOMPtr<Document> doc = aWindow.GetExtantDoc()) {
    if (nsPresContext* presContext = doc->GetPresContext()) {
      if (presContext->IsRootContentDocumentCrossProcess()) {
        // Use eMainThread so this takes precedence over session history
        // (ScrollFrameHelper::ScrollToRestoredPosition()).
        presContext->PresShell()->ScrollToVisual(
            CSSPoint::ToAppUnits(CSSPoint(pos_X, pos_Y)),
            layers::FrameMetrics::eMainThread, ScrollMode::Instant);
      }
    }
  }
}

// Implements the Luhn checksum algorithm as described at
// http://wikipedia.org/wiki/Luhn_algorithm
// Number digit lengths vary with network, but should fall within 12-19 range.
// [2] More details at https://en.wikipedia.org/wiki/Payment_card_number
static bool IsValidCCNumber(nsAString& aValue) {
  uint32_t total = 0;
  uint32_t numLength = 0;
  uint32_t strLen = aValue.Length();
  for (uint32_t i = 0; i < strLen; ++i) {
    uint32_t idx = strLen - i - 1;
    // ignore whitespace and dashes)
    char16_t chr = aValue[idx];
    if (IsSpaceCharacter(chr) || chr == '-') {
      continue;
    }
    // If our number is too long, note that fact
    ++numLength;
    if (numLength > 19) {
      return false;
    }
    // Try to parse the character as a base-10 integer.
    nsresult rv = NS_OK;
    uint32_t val = Substring(aValue, idx, 1).ToInteger(&rv, 10);
    if (NS_FAILED(rv)) {
      return false;
    }
    if (i % 2 == 1) {
      val *= 2;
      if (val > 9) {
        val -= 9;
      }
    }
    total += val;
  }

  return numLength >= 12 && total % 10 == 0;
}

// Limit the number of XPath expressions for performance reasons. See bug
// 477564.
static const uint16_t kMaxTraversedXPaths = 100;

// A helper function to append a element into mId or mXpath of CollectedData
static Record<nsString, OwningStringOrBooleanOrObject>::EntryType*
AppendEntryToCollectedData(nsINode* aNode, const nsAString& aId,
                           uint16_t& aGeneratedCount,
                           Nullable<CollectedData>& aRetVal) {
  Record<nsString, OwningStringOrBooleanOrObject>::EntryType* entry;
  if (!aId.IsEmpty()) {
    if (!aRetVal.SetValue().mId.WasPassed()) {
      aRetVal.SetValue().mId.Construct();
    }
    auto& recordEntries = aRetVal.SetValue().mId.Value().Entries();
    entry = recordEntries.AppendElement();
    entry->mKey = aId;
  } else {
    if (!aRetVal.SetValue().mXpath.WasPassed()) {
      aRetVal.SetValue().mXpath.Construct();
    }
    auto& recordEntries = aRetVal.SetValue().mXpath.Value().Entries();
    entry = recordEntries.AppendElement();
    nsAutoString xpath;
    aNode->GenerateXPath(xpath);
    aGeneratedCount++;
    entry->mKey = xpath;
  }
  return entry;
}

/* for bool value */
static void AppendValueToCollectedData(nsINode* aNode, const nsAString& aId,
                                       const bool& aValue,
                                       uint16_t& aGeneratedCount,
                                       JSContext* aCx,
                                       Nullable<CollectedData>& aRetVal) {
  Record<nsString, OwningStringOrBooleanOrObject>::EntryType* entry =
      AppendEntryToCollectedData(aNode, aId, aGeneratedCount, aRetVal);
  entry->mValue.SetAsBoolean() = aValue;
}

/* for nsString value */
static void AppendValueToCollectedData(nsINode* aNode, const nsAString& aId,
                                       const nsString& aValue,
                                       uint16_t& aGeneratedCount,
                                       Nullable<CollectedData>& aRetVal) {
  Record<nsString, OwningStringOrBooleanOrObject>::EntryType* entry =
      AppendEntryToCollectedData(aNode, aId, aGeneratedCount, aRetVal);
  entry->mValue.SetAsString() = aValue;
}

/* for single select value */
static void AppendValueToCollectedData(
    nsINode* aNode, const nsAString& aId,
    const CollectedNonMultipleSelectValue& aValue, uint16_t& aGeneratedCount,
    JSContext* aCx, Nullable<CollectedData>& aRetVal) {
  JS::Rooted<JS::Value> jsval(aCx);
  if (!ToJSValue(aCx, aValue, &jsval)) {
    JS_ClearPendingException(aCx);
    return;
  }
  Record<nsString, OwningStringOrBooleanOrObject>::EntryType* entry =
      AppendEntryToCollectedData(aNode, aId, aGeneratedCount, aRetVal);
  entry->mValue.SetAsObject() = &jsval.toObject();
}

/* special handing for input element with string type */
static void AppendValueToCollectedData(Document& aDocument, nsINode* aNode,
                                       const nsAString& aId,
                                       const nsString& aValue,
                                       uint16_t& aGeneratedCount,
                                       JSContext* aCx,
                                       Nullable<CollectedData>& aRetVal) {
  if (!aId.IsEmpty()) {
    // We want to avoid saving data for about:sessionrestore as a string.
    // Since it's stored in the form as stringified JSON, stringifying
    // further causes an explosion of escape characters. cf. bug 467409
    if (aId.EqualsLiteral("sessionData")) {
      nsAutoCString url;
      Unused << aDocument.GetDocumentURI()->GetSpecIgnoringRef(url);
      if (url.EqualsLiteral("about:sessionrestore") ||
          url.EqualsLiteral("about:welcomeback")) {
        JS::Rooted<JS::Value> jsval(aCx);
        if (JS_ParseJSON(aCx, aValue.get(), aValue.Length(), &jsval) &&
            jsval.isObject()) {
          Record<nsString, OwningStringOrBooleanOrObject>::EntryType* entry =
              AppendEntryToCollectedData(aNode, aId, aGeneratedCount, aRetVal);
          entry->mValue.SetAsObject() = &jsval.toObject();
        } else {
          JS_ClearPendingException(aCx);
        }
        return;
      }
    }
  }
  AppendValueToCollectedData(aNode, aId, aValue, aGeneratedCount, aRetVal);
}

/* for nsTArray<nsString>: file and multipleSelect */
static void AppendValueToCollectedData(nsINode* aNode, const nsAString& aId,
                                       const nsAString& aValueType,
                                       nsTArray<nsString>& aValue,
                                       uint16_t& aGeneratedCount,
                                       JSContext* aCx,
                                       Nullable<CollectedData>& aRetVal) {
  JS::Rooted<JS::Value> jsval(aCx);
  if (aValueType.EqualsLiteral("file")) {
    CollectedFileListValue val;
    val.mType = aValueType;
    val.mFileList = std::move(aValue);
    if (!ToJSValue(aCx, val, &jsval)) {
      JS_ClearPendingException(aCx);
      return;
    }
  } else {
    if (!ToJSValue(aCx, aValue, &jsval)) {
      JS_ClearPendingException(aCx);
      return;
    }
  }
  Record<nsString, OwningStringOrBooleanOrObject>::EntryType* entry =
      AppendEntryToCollectedData(aNode, aId, aGeneratedCount, aRetVal);
  entry->mValue.SetAsObject() = &jsval.toObject();
}

/* For form-associated custom element state */
static void AppendValueToCollectedData(
    nsINode* aNode, const nsAString& aId,
    const Nullable<OwningFileOrUSVStringOrFormData>& aValue,
    const Nullable<OwningFileOrUSVStringOrFormData>& aState,
    uint16_t& aGeneratedCount, JSContext* aCx,
    Nullable<CollectedData>& aRetVal) {
  Record<nsString, OwningStringOrBooleanOrObject>::EntryType* entry =
      AppendEntryToCollectedData(aNode, aId, aGeneratedCount, aRetVal);

  CollectedCustomElementValue val;
  val.mValue = aValue;
  val.mState = aState;
  JS::Rooted<JS::Value> jsval(aCx);
  if (!ToJSValue(aCx, val, &jsval)) {
    JS_ClearPendingException(aCx);
    return;
  }
  entry->mValue.SetAsObject() = &jsval.toObject();
}

// This isn't size as in binary size, just a heuristic to not store too large
// fields in session store. See StaticPrefs::browser_sessionstore_dom_form_limit
static uint32_t SizeOfFormEntry(const FormEntryValue& aValue) {
  uint32_t size = 0;
  switch (aValue.type()) {
    case FormEntryValue::TCheckbox:
      size = aValue.get_Checkbox().value() ? 4 : 5;
      break;
    case FormEntryValue::TTextField:
      size = aValue.get_TextField().value().Length();
      break;
    case FormEntryValue::TFileList: {
      for (const auto& value : aValue.get_FileList().valueList()) {
        size += value.Length();
      }
      break;
    }
    case FormEntryValue::TSingleSelect:
      size = aValue.get_SingleSelect().value().Length();
      break;
    case FormEntryValue::TMultipleSelect: {
      for (const auto& value : aValue.get_MultipleSelect().valueList()) {
        size += value.Length();
      }
      break;
    }
    case FormEntryValue::TCustomElementTuple: {
      auto customElementTupleSize =
          [](const CustomElementFormValue& value) -> uint32_t {
        switch (value.type()) {
          case CustomElementFormValue::TBlobImpl:
            return value.get_BlobImpl()->GetAllocationSize();
          case CustomElementFormValue::TnsString:
            return value.get_nsString().Length();
          case CustomElementFormValue::TArrayOfFormDataTuple: {
            uint32_t formDataSize = 0;
            for (const auto& entry : value.get_ArrayOfFormDataTuple()) {
              formDataSize += entry.name().Length();
              const auto& entryValue = entry.value();
              switch (entryValue.type()) {
                case FormDataValue::TBlobImpl:
                  formDataSize +=
                      entryValue.get_BlobImpl()->GetAllocationSize();
                  break;
                case FormDataValue::TnsString:
                  formDataSize += entryValue.get_nsString().Length();
                  break;
                default:
                  break;
              }
            }
            return formDataSize;
          }
          default:
            return 0;
        }
      };

      auto ceTuple = aValue.get_CustomElementTuple();
      size += customElementTupleSize(ceTuple.value());
      size += customElementTupleSize(ceTuple.state());
      break;
    }
    default:
      break;
  }
  return size;
}

static uint32_t AppendEntry(nsINode* aNode, const nsString& aId,
                            const FormEntryValue& aValue,
                            sessionstore::FormData& aFormData) {
  uint32_t size = SizeOfFormEntry(aValue);
  if (size > StaticPrefs::browser_sessionstore_dom_form_limit()) {
    return 0;
  }

  if (aId.IsEmpty()) {
    FormEntry* entry = aFormData.xpath().AppendElement();
    entry->value() = aValue;
    aNode->GenerateXPath(entry->id());
    size += entry->id().Length();
  } else {
    aFormData.id().AppendElement(FormEntry{aId, aValue});
    size += aId.Length();
  }

  return size;
}

static uint32_t CollectTextAreaElement(Document* aDocument,
                                       sessionstore::FormData& aFormData) {
  uint32_t size = 0;
  RefPtr<nsContentList> textlist =
      NS_GetContentList(aDocument, kNameSpaceID_XHTML, u"textarea"_ns);
  uint32_t length = textlist->Length();
  for (uint32_t i = 0; i < length; ++i) {
    MOZ_ASSERT(textlist->Item(i), "null item in node list!");

    auto* textArea = HTMLTextAreaElement::FromNodeOrNull(textlist->Item(i));
    if (!textArea) {
      continue;
    }
    DOMString autocomplete;
    textArea->GetAutocomplete(autocomplete);
    if (autocomplete.AsAString().EqualsLiteral("off")) {
      continue;
    }
    nsAutoString id;
    textArea->GetId(id);
    if (id.IsEmpty() && (aFormData.xpath().Length() > kMaxTraversedXPaths)) {
      continue;
    }
    nsString value;
    textArea->GetValue(value);
    // In order to reduce XPath generation (which is slow), we only save data
    // for form fields that have been changed. (cf. bug 537289)
    if (textArea->AttrValueIs(kNameSpaceID_None, nsGkAtoms::value, value,
                              eCaseMatters)) {
      continue;
    }

    size += AppendEntry(textArea, id, TextField{value}, aFormData);
  }

  return size;
}

static uint32_t CollectInputElement(Document* aDocument,
                                    sessionstore::FormData& aFormData) {
  uint32_t size = 0;
  RefPtr<nsContentList> inputlist =
      NS_GetContentList(aDocument, kNameSpaceID_XHTML, u"input"_ns);
  uint32_t length = inputlist->Length();
  for (uint32_t i = 0; i < length; ++i) {
    MOZ_ASSERT(inputlist->Item(i), "null item in node list!");
    nsCOMPtr<nsIFormControl> formControl =
        do_QueryInterface(inputlist->Item(i));
    if (formControl) {
      auto controlType = formControl->ControlType();
      if (controlType == FormControlType::InputPassword ||
          controlType == FormControlType::InputHidden ||
          controlType == FormControlType::InputButton ||
          controlType == FormControlType::InputImage ||
          controlType == FormControlType::InputSubmit ||
          controlType == FormControlType::InputReset) {
        continue;
      }
    }
    RefPtr<HTMLInputElement> input =
        HTMLInputElement::FromNodeOrNull(inputlist->Item(i));
    if (!input || !nsContentUtils::IsAutocompleteEnabled(input)) {
      continue;
    }
    nsAutoString id;
    input->GetId(id);
    if (id.IsEmpty() && (aFormData.xpath().Length() > kMaxTraversedXPaths)) {
      continue;
    }
    Nullable<AutocompleteInfo> aInfo;
    input->GetAutocompleteInfo(aInfo);
    if (!aInfo.IsNull() && !aInfo.Value().mCanAutomaticallyPersist) {
      continue;
    }

    FormEntryValue value;
    if (input->ControlType() == FormControlType::InputCheckbox ||
        input->ControlType() == FormControlType::InputRadio) {
      bool checked = input->Checked();
      if (checked == input->DefaultChecked()) {
        continue;
      }
      size += AppendEntry(input, id, Checkbox{checked}, aFormData);
    } else if (input->ControlType() == FormControlType::InputFile) {
      IgnoredErrorResult rv;
      sessionstore::FileList file;
      input->MozGetFileNameArray(file.valueList(), rv);
      if (rv.Failed() || file.valueList().IsEmpty()) {
        continue;
      }
      size += AppendEntry(input, id, file, aFormData);
    } else {
      TextField field;
      input->GetValue(field.value(), CallerType::System);
      auto& value = field.value();
      // In order to reduce XPath generation (which is slow), we only save data
      // for form fields that have been changed. (cf. bug 537289)
      // Also, don't want to collect credit card number.
      if (value.IsEmpty() || IsValidCCNumber(value) ||
          input->HasBeenTypePassword() ||
          input->AttrValueIs(kNameSpaceID_None, nsGkAtoms::value, value,
                             eCaseMatters)) {
        continue;
      }
      size += AppendEntry(input, id, field, aFormData);
    }
  }

  return size;
}

static uint32_t CollectSelectElement(Document* aDocument,
                                     sessionstore::FormData& aFormData) {
  uint32_t size = 0;
  RefPtr<nsContentList> selectlist =
      NS_GetContentList(aDocument, kNameSpaceID_XHTML, u"select"_ns);
  uint32_t length = selectlist->Length();
  for (uint32_t i = 0; i < length; ++i) {
    MOZ_ASSERT(selectlist->Item(i), "null item in node list!");
    RefPtr<HTMLSelectElement> select =
        HTMLSelectElement::FromNodeOrNull(selectlist->Item(i));
    if (!select) {
      continue;
    }
    nsAutoString id;
    select->GetId(id);
    if (id.IsEmpty() && (aFormData.xpath().Length() > kMaxTraversedXPaths)) {
      continue;
    }
    AutocompleteInfo aInfo;
    select->GetAutocompleteInfo(aInfo);
    if (!aInfo.mCanAutomaticallyPersist) {
      continue;
    }

    if (!select->Multiple()) {
      HTMLOptionsCollection* options = select->GetOptions();
      if (!options) {
        continue;
      }

      uint32_t numOptions = options->Length();
      int32_t defaultIndex = 0;
      for (uint32_t idx = 0; idx < numOptions; idx++) {
        HTMLOptionElement* option = options->ItemAsOption(idx);
        if (option->DefaultSelected()) {
          defaultIndex = option->Index();
        }
      }

      int32_t selectedIndex = select->SelectedIndex();
      if (selectedIndex == defaultIndex || selectedIndex < 0) {
        continue;
      }

      DOMString selectVal;
      select->GetValue(selectVal);
      size += AppendEntry(select, id,
                          SingleSelect{static_cast<uint32_t>(selectedIndex),
                                       selectVal.AsAString()},
                          aFormData);
    } else {
      HTMLOptionsCollection* options = select->GetOptions();
      if (!options) {
        continue;
      }
      bool hasDefaultValue = true;
      nsTArray<nsString> selectslist;
      uint32_t numOptions = options->Length();
      for (uint32_t idx = 0; idx < numOptions; idx++) {
        HTMLOptionElement* option = options->ItemAsOption(idx);
        bool selected = option->Selected();

        hasDefaultValue =
            hasDefaultValue && (selected == option->DefaultSelected());

        if (!selected) {
          continue;
        }
        option->GetValue(*selectslist.AppendElement());
      }
      // In order to reduce XPath generation (which is slow), we only save data
      // for form fields that have been changed. (cf. bug 537289)
      if (hasDefaultValue) {
        continue;
      }

      size += AppendEntry(select, id, MultipleSelect{selectslist}, aFormData);
    }
  }

  return size;
}

static already_AddRefed<nsContentList> GetFormAssociatedCustomElements(
    nsINode* aRootNode) {
  MOZ_ASSERT(aRootNode, "Content list has to have a root");

  auto matchFunc = [](Element* aElement, int32_t aNamespace, nsAtom* aAtom,
                      void* aData) -> bool {
    return aElement->HasCustomElementData() &&
           aElement->GetCustomElementData()->IsFormAssociated();
  };
  RefPtr<nsContentList> list =
      new nsContentList(aRootNode, matchFunc, nullptr, nullptr);
  return list.forget();
}

static uint32_t CollectFormAssociatedCustomElement(
    Document* aDocument, sessionstore::FormData& aFormData) {
  uint32_t size = 0;
  RefPtr<nsContentList> faceList = GetFormAssociatedCustomElements(aDocument);
  uint32_t length = faceList->Length();
  for (uint32_t i = 0; i < length; ++i) {
    MOZ_ASSERT(faceList->Item(i), "null item in node list!");
    RefPtr<Element> element = Element::FromNode(faceList->Item(i));

    nsAutoString id;
    element->GetId(id);
    if (id.IsEmpty() && (aFormData.xpath().Length() > kMaxTraversedXPaths)) {
      continue;
    }

    const auto* internals =
        element->GetCustomElementData()->GetElementInternals();
    auto formState = internals->GetFormState();
    auto formValue = internals->GetFormSubmissionValue();
    if (formState.IsNull() && formValue.IsNull()) {
      continue;
    }

    CustomElementTuple entry;
    entry.value() = nsContentUtils::ConvertToCustomElementFormValue(formValue);
    entry.state() = nsContentUtils::ConvertToCustomElementFormValue(formState);
    size += AppendEntry(element, id, entry, aFormData);
  }
  return size;
}

/* static */
uint32_t SessionStoreUtils::CollectFormData(Document* aDocument,
                                            sessionstore::FormData& aFormData) {
  MOZ_DIAGNOSTIC_ASSERT(aDocument);
  uint32_t size = 0;
  size += CollectTextAreaElement(aDocument, aFormData);
  size += CollectInputElement(aDocument, aFormData);
  size += CollectSelectElement(aDocument, aFormData);
  size += CollectFormAssociatedCustomElement(aDocument, aFormData);

  aFormData.hasData() =
      !aFormData.id().IsEmpty() || !aFormData.xpath().IsEmpty();

  return size;
}

/* static */
template <typename... ArgsT>
void SessionStoreUtils::CollectFromTextAreaElement(Document& aDocument,
                                                   uint16_t& aGeneratedCount,
                                                   ArgsT&&... args) {
  RefPtr<nsContentList> textlist =
      NS_GetContentList(&aDocument, kNameSpaceID_XHTML, u"textarea"_ns);
  uint32_t length = textlist->Length(true);
  for (uint32_t i = 0; i < length; ++i) {
    MOZ_ASSERT(textlist->Item(i), "null item in node list!");

    HTMLTextAreaElement* textArea =
        HTMLTextAreaElement::FromNodeOrNull(textlist->Item(i));
    if (!textArea) {
      continue;
    }
    DOMString autocomplete;
    textArea->GetAutocomplete(autocomplete);
    if (autocomplete.AsAString().EqualsLiteral("off")) {
      continue;
    }
    nsAutoString id;
    textArea->GetId(id);
    if (id.IsEmpty() && (aGeneratedCount > kMaxTraversedXPaths)) {
      continue;
    }
    nsString value;
    textArea->GetValue(value);
    // In order to reduce XPath generation (which is slow), we only save data
    // for form fields that have been changed. (cf. bug 537289)
    if (textArea->AttrValueIs(kNameSpaceID_None, nsGkAtoms::value, value,
                              eCaseMatters)) {
      continue;
    }
    AppendValueToCollectedData(textArea, id, value, aGeneratedCount,
                               std::forward<ArgsT>(args)...);
  }
}

/* static */
template <typename... ArgsT>
void SessionStoreUtils::CollectFromInputElement(Document& aDocument,
                                                uint16_t& aGeneratedCount,
                                                ArgsT&&... args) {
  RefPtr<nsContentList> inputlist =
      NS_GetContentList(&aDocument, kNameSpaceID_XHTML, u"input"_ns);
  uint32_t length = inputlist->Length(true);
  for (uint32_t i = 0; i < length; ++i) {
    MOZ_ASSERT(inputlist->Item(i), "null item in node list!");
    nsCOMPtr<nsIFormControl> formControl =
        do_QueryInterface(inputlist->Item(i));
    if (formControl) {
      auto controlType = formControl->ControlType();
      if (controlType == FormControlType::InputPassword ||
          controlType == FormControlType::InputHidden ||
          controlType == FormControlType::InputButton ||
          controlType == FormControlType::InputImage ||
          controlType == FormControlType::InputSubmit ||
          controlType == FormControlType::InputReset) {
        continue;
      }
    }
    RefPtr<HTMLInputElement> input =
        HTMLInputElement::FromNodeOrNull(inputlist->Item(i));
    if (!input || !nsContentUtils::IsAutocompleteEnabled(input)) {
      continue;
    }
    nsAutoString id;
    input->GetId(id);
    if (id.IsEmpty() && (aGeneratedCount > kMaxTraversedXPaths)) {
      continue;
    }
    Nullable<AutocompleteInfo> aInfo;
    input->GetAutocompleteInfo(aInfo);
    if (!aInfo.IsNull() && !aInfo.Value().mCanAutomaticallyPersist) {
      continue;
    }

    if (input->ControlType() == FormControlType::InputCheckbox ||
        input->ControlType() == FormControlType::InputRadio) {
      bool checked = input->Checked();
      if (checked == input->DefaultChecked()) {
        continue;
      }
      AppendValueToCollectedData(input, id, checked, aGeneratedCount,
                                 std::forward<ArgsT>(args)...);
    } else if (input->ControlType() == FormControlType::InputFile) {
      IgnoredErrorResult rv;
      nsTArray<nsString> result;
      input->MozGetFileNameArray(result, rv);
      if (rv.Failed() || result.Length() == 0) {
        continue;
      }
      AppendValueToCollectedData(input, id, u"file"_ns, result, aGeneratedCount,
                                 std::forward<ArgsT>(args)...);
    } else {
      nsString value;
      input->GetValue(value, CallerType::System);
      // In order to reduce XPath generation (which is slow), we only save data
      // for form fields that have been changed. (cf. bug 537289)
      // Also, don't want to collect credit card number.
      if (value.IsEmpty() || IsValidCCNumber(value) ||
          input->HasBeenTypePassword() ||
          input->AttrValueIs(kNameSpaceID_None, nsGkAtoms::value, value,
                             eCaseMatters)) {
        continue;
      }
      AppendValueToCollectedData(aDocument, input, id, value, aGeneratedCount,
                                 std::forward<ArgsT>(args)...);
    }
  }
}

/* static */
template <typename... ArgsT>
void SessionStoreUtils::CollectFromSelectElement(Document& aDocument,
                                                 uint16_t& aGeneratedCount,
                                                 ArgsT&&... args) {
  RefPtr<nsContentList> selectlist =
      NS_GetContentList(&aDocument, kNameSpaceID_XHTML, u"select"_ns);
  uint32_t length = selectlist->Length(true);
  for (uint32_t i = 0; i < length; ++i) {
    MOZ_ASSERT(selectlist->Item(i), "null item in node list!");
    RefPtr<HTMLSelectElement> select =
        HTMLSelectElement::FromNodeOrNull(selectlist->Item(i));
    if (!select) {
      continue;
    }
    nsAutoString id;
    select->GetId(id);
    if (id.IsEmpty() && (aGeneratedCount > kMaxTraversedXPaths)) {
      continue;
    }
    AutocompleteInfo aInfo;
    select->GetAutocompleteInfo(aInfo);
    if (!aInfo.mCanAutomaticallyPersist) {
      continue;
    }
    nsAutoCString value;
    if (!select->Multiple()) {
      // <select>s without the multiple attribute are hard to determine the
      // default value, so assume we don't have the default.
      DOMString selectVal;
      select->GetValue(selectVal);
      CollectedNonMultipleSelectValue val;
      val.mSelectedIndex = select->SelectedIndex();
      val.mValue = selectVal.AsAString();
      AppendValueToCollectedData(select, id, val, aGeneratedCount,
                                 std::forward<ArgsT>(args)...);
    } else {
      // <select>s with the multiple attribute are easier to determine the
      // default value since each <option> has a defaultSelected property
      HTMLOptionsCollection* options = select->GetOptions();
      if (!options) {
        continue;
      }
      bool hasDefaultValue = true;
      nsTArray<nsString> selectslist;
      uint32_t numOptions = options->Length();
      for (uint32_t idx = 0; idx < numOptions; idx++) {
        HTMLOptionElement* option = options->ItemAsOption(idx);
        bool selected = option->Selected();
        if (!selected) {
          continue;
        }
        option->GetValue(*selectslist.AppendElement());
        hasDefaultValue =
            hasDefaultValue && (selected == option->DefaultSelected());
      }
      // In order to reduce XPath generation (which is slow), we only save data
      // for form fields that have been changed. (cf. bug 537289)
      if (hasDefaultValue) {
        continue;
      }

      AppendValueToCollectedData(select, id, u"multipleSelect"_ns, selectslist,
                                 aGeneratedCount, std::forward<ArgsT>(args)...);
    }
  }
}

/* static */
template <typename... ArgsT>
void SessionStoreUtils::CollectFromFormAssociatedCustomElement(
    Document& aDocument, uint16_t& aGeneratedCount, ArgsT&&... args) {
  RefPtr<nsContentList> faceList = GetFormAssociatedCustomElements(&aDocument);
  uint32_t length = faceList->Length(true);
  for (uint32_t i = 0; i < length; ++i) {
    MOZ_ASSERT(faceList->Item(i), "null item in node list!");
    RefPtr<Element> element = Element::FromNode(faceList->Item(i));

    nsAutoString id;
    element->GetId(id);
    if (id.IsEmpty() && (aGeneratedCount > kMaxTraversedXPaths)) {
      continue;
    }

    auto* internals = element->GetCustomElementData()->GetElementInternals();
    const auto& state = internals->GetFormState();
    const auto& value = internals->GetFormSubmissionValue();
    if (state.IsNull() && value.IsNull()) {
      continue;
    }

    AppendValueToCollectedData(element, id, value, state, aGeneratedCount,
                               std::forward<ArgsT>(args)...);
  }
}

static void CollectCurrentFormData(JSContext* aCx, Document& aDocument,
                                   Nullable<CollectedData>& aRetVal) {
  uint16_t generatedCount = 0;
  /* textarea element */
  SessionStoreUtils::CollectFromTextAreaElement(aDocument, generatedCount,
                                                aRetVal);
  /* input element */
  SessionStoreUtils::CollectFromInputElement(aDocument, generatedCount, aCx,
                                             aRetVal);
  /* select element */
  SessionStoreUtils::CollectFromSelectElement(aDocument, generatedCount, aCx,
                                              aRetVal);
  /* form-associated custom element */
  SessionStoreUtils::CollectFromFormAssociatedCustomElement(
      aDocument, generatedCount, aCx, aRetVal);

  Element* bodyElement = aDocument.GetBody();
  if (bodyElement && bodyElement->IsInDesignMode()) {
    bodyElement->GetInnerHTML(aRetVal.SetValue().mInnerHTML.Construct(),
                              IgnoreErrors());
  }

  if (aRetVal.IsNull()) {
    return;
  }

  // Store the frame's current URL with its form data so that we can compare
  // it when restoring data to not inject form data into the wrong document.
  nsIURI* uri = aDocument.GetDocumentURI();
  if (uri) {
    uri->GetSpecIgnoringRef(aRetVal.SetValue().mUrl.Construct());
  }
}

MOZ_CAN_RUN_SCRIPT
static void SetElementAsString(Element* aElement, const nsAString& aValue) {
  IgnoredErrorResult rv;
  if (auto* textArea = HTMLTextAreaElement::FromNode(aElement)) {
    // Known live because `aElement` is known live.
    MOZ_KnownLive(textArea)->SetValue(aValue, rv);
    if (!rv.Failed()) {
      nsContentUtils::DispatchInputEvent(aElement);
    }
    return;
  }
  if (auto* input = HTMLInputElement::FromNode(aElement)) {
    input->SetValue(aValue, CallerType::NonSystem, rv);
    if (!rv.Failed()) {
      nsContentUtils::DispatchInputEvent(aElement);
    }
    return;
  }
}

MOZ_CAN_RUN_SCRIPT
static void SetElementAsBool(Element* aElement, bool aValue) {
  HTMLInputElement* input = HTMLInputElement::FromNode(aElement);
  if (input) {
    bool checked = input->Checked();
    if (aValue != checked) {
      input->SetChecked(aValue);
      nsContentUtils::DispatchInputEvent(aElement);
    }
  }
}

MOZ_CAN_RUN_SCRIPT
static void SetElementAsFiles(HTMLInputElement* aElement,
                              const CollectedFileListValue& aValue) {
  IgnoredErrorResult rv;
  aElement->MozSetFileNameArray(aValue.mFileList, rv);
  if (rv.Failed()) {
    return;
  }
  nsContentUtils::DispatchInputEvent(aElement);
}

MOZ_CAN_RUN_SCRIPT
static void SetElementAsSelect(HTMLSelectElement* aElement,
                               const CollectedNonMultipleSelectValue& aValue) {
  HTMLOptionsCollection* options = aElement->GetOptions();
  if (!options) {
    return;
  }
  int32_t selectIdx = options->SelectedIndex();
  if (selectIdx >= 0) {
    nsAutoString selectOptionVal;
    options->ItemAsOption(selectIdx)->GetValue(selectOptionVal);
    if (aValue.mValue.Equals(selectOptionVal)) {
      return;
    }
  }
  uint32_t numOptions = options->Length();
  for (uint32_t idx = 0; idx < numOptions; idx++) {
    HTMLOptionElement* option = options->ItemAsOption(idx);
    nsAutoString optionValue;
    option->GetValue(optionValue);
    if (aValue.mValue.Equals(optionValue)) {
      aElement->SetSelectedIndex(idx);
      nsContentUtils::DispatchInputEvent(aElement);
    }
  }
}

MOZ_CAN_RUN_SCRIPT
static void SetElementAsMultiSelect(HTMLSelectElement* aElement,
                                    const nsTArray<nsString>& aValueArray) {
  bool fireEvent = false;
  HTMLOptionsCollection* options = aElement->GetOptions();
  if (!options) {
    return;
  }
  uint32_t numOptions = options->Length();
  for (uint32_t idx = 0; idx < numOptions; idx++) {
    HTMLOptionElement* option = options->ItemAsOption(idx);
    nsAutoString optionValue;
    option->GetValue(optionValue);
    for (uint32_t i = 0, l = aValueArray.Length(); i < l; ++i) {
      if (optionValue.Equals(aValueArray[i])) {
        option->SetSelected(true);
        if (!option->DefaultSelected()) {
          fireEvent = true;
        }
      }
    }
  }
  if (fireEvent) {
    nsContentUtils::DispatchInputEvent(aElement);
  }
}

MOZ_CAN_RUN_SCRIPT
static void SetElementAsObject(JSContext* aCx, Element* aElement,
                               JS::Handle<JS::Value> aObject) {
  RefPtr<HTMLInputElement> input = HTMLInputElement::FromNode(aElement);
  if (input) {
    if (input->ControlType() == FormControlType::InputFile) {
      CollectedFileListValue value;
      if (value.Init(aCx, aObject)) {
        SetElementAsFiles(input, value);
      } else {
        JS_ClearPendingException(aCx);
      }
    }
    return;
  }
  RefPtr<HTMLSelectElement> select = HTMLSelectElement::FromNode(aElement);
  if (select) {
    // For Single Select Element
    if (!select->Multiple()) {
      CollectedNonMultipleSelectValue value;
      if (value.Init(aCx, aObject)) {
        SetElementAsSelect(select, value);
      } else {
        JS_ClearPendingException(aCx);
      }
      return;
    }

    // For Multiple Selects Element
    bool isArray = false;
    JS::IsArrayObject(aCx, aObject, &isArray);
    if (!isArray) {
      return;
    }
    JS::Rooted<JSObject*> arrayObj(aCx, &aObject.toObject());
    uint32_t arrayLength = 0;
    if (!JS::GetArrayLength(aCx, arrayObj, &arrayLength)) {
      JS_ClearPendingException(aCx);
      return;
    }
    nsTArray<nsString> array(arrayLength);
    for (uint32_t arrayIdx = 0; arrayIdx < arrayLength; arrayIdx++) {
      JS::Rooted<JS::Value> element(aCx);
      if (!JS_GetElement(aCx, arrayObj, arrayIdx, &element)) {
        JS_ClearPendingException(aCx);
        return;
      }
      if (!element.isString()) {
        return;
      }
      nsAutoJSString value;
      if (!value.init(aCx, element)) {
        JS_ClearPendingException(aCx);
        return;
      }
      array.AppendElement(value);
    }
    SetElementAsMultiSelect(select, array);
  }

  // For Form-Associated Custom Element:
  if (!aObject.isObject()) {
    // Don't restore null values.
    return;
  }

  auto* data = aElement->GetCustomElementData();
  if (!data || !data->IsFormAssociated()) {
    return;
  }
  auto* internals = data->GetElementInternals();

  CollectedCustomElementValue value;
  if (!value.Init(aCx, aObject)) {
    JS_ClearPendingException(aCx);
    return;
  }
  internals->RestoreFormValue(std::move(value.mValue), std::move(value.mState));
}

MOZ_CAN_RUN_SCRIPT
static void SetSessionData(JSContext* aCx, Element* aElement,
                           JS::MutableHandle<JS::Value> aObject) {
  nsAutoString data;
  if (nsContentUtils::StringifyJSON(aCx, aObject, data,
                                    UndefinedIsNullStringLiteral)) {
    SetElementAsString(aElement, data);
  } else {
    JS_ClearPendingException(aCx);
  }
}

MOZ_CAN_RUN_SCRIPT
static void SetInnerHTML(Document& aDocument, const nsString& aInnerHTML) {
  RefPtr<Element> bodyElement = aDocument.GetBody();
  if (bodyElement && bodyElement->IsInDesignMode()) {
    IgnoredErrorResult rv;
    bodyElement->SetInnerHTML(aInnerHTML, aDocument.NodePrincipal(), rv);
    if (!rv.Failed()) {
      nsContentUtils::DispatchInputEvent(bodyElement);
    }
  }
}

class FormDataParseContext : public txIParseContext {
 public:
  explicit FormDataParseContext(bool aCaseInsensitive)
      : mIsCaseInsensitive(aCaseInsensitive) {}

  nsresult resolveNamespacePrefix(nsAtom* aPrefix, int32_t& aID) override {
    if (aPrefix == nsGkAtoms::xul) {
      aID = kNameSpaceID_XUL;
    } else {
      MOZ_ASSERT(nsDependentAtomString(aPrefix).EqualsLiteral("xhtml"));
      aID = kNameSpaceID_XHTML;
    }
    return NS_OK;
  }

  nsresult resolveFunctionCall(nsAtom* aName, int32_t aID,
                               FunctionCall** aFunction) override {
    return NS_ERROR_XPATH_UNKNOWN_FUNCTION;
  }

  bool caseInsensitiveNameTests() override { return mIsCaseInsensitive; }

  void SetErrorOffset(uint32_t aOffset) override {}

 private:
  bool mIsCaseInsensitive;
};

static Element* FindNodeByXPath(Document& aDocument,
                                const nsAString& aExpression) {
  FormDataParseContext parsingContext(aDocument.IsHTMLDocument());
  IgnoredErrorResult rv;
  UniquePtr<XPathExpression> expression(
      aDocument.XPathEvaluator()->CreateExpression(aExpression, &parsingContext,
                                                   &aDocument, rv));
  if (rv.Failed()) {
    return nullptr;
  }
  RefPtr<XPathResult> result = expression->Evaluate(
      aDocument, XPathResult::FIRST_ORDERED_NODE_TYPE, nullptr, rv);
  if (rv.Failed()) {
    return nullptr;
  }
  return Element::FromNodeOrNull(result->GetSingleNodeValue(rv));
}

/* static */
bool SessionStoreUtils::RestoreFormData(const GlobalObject& aGlobal,
                                        Document& aDocument,
                                        const CollectedData& aData) {
  if (!aData.mUrl.WasPassed()) {
    return true;
  }

  // Don't restore any data for the given frame if the URL
  // stored in the form data doesn't match its current URL.
  nsAutoCString url;
  Unused << aDocument.GetDocumentURI()->GetSpecIgnoringRef(url);
  if (!aData.mUrl.Value().Equals(url)) {
    return false;
  }

  using Change = SessionStoreChangeListener::Change;
  SessionStoreChangeListener::CollectSessionStoreData(
      aDocument.GetWindowContext(), EnumSet<Change>(Change::Input));

  if (aData.mInnerHTML.WasPassed()) {
    SetInnerHTML(aDocument, aData.mInnerHTML.Value());
  }
  if (aData.mId.WasPassed()) {
    for (auto& entry : aData.mId.Value().Entries()) {
      RefPtr<Element> node = aDocument.GetElementById(entry.mKey);
      if (node == nullptr) {
        continue;
      }
      if (entry.mValue.IsString()) {
        SetElementAsString(node, entry.mValue.GetAsString());
      } else if (entry.mValue.IsBoolean()) {
        SetElementAsBool(node, entry.mValue.GetAsBoolean());
      } else {
        // For about:{sessionrestore,welcomeback} we saved the field as JSON to
        // avoid nested instances causing humongous sessionstore.js files.
        // cf. bug 467409
        JSContext* cx = aGlobal.Context();
        if (entry.mKey.EqualsLiteral("sessionData")) {
          if (url.EqualsLiteral("about:sessionrestore") ||
              url.EqualsLiteral("about:welcomeback")) {
            JS::Rooted<JS::Value> object(
                cx, JS::ObjectValue(*entry.mValue.GetAsObject()));
            SetSessionData(cx, node, &object);
            continue;
          }
        }
        JS::Rooted<JS::Value> object(
            cx, JS::ObjectValue(*entry.mValue.GetAsObject()));
        SetElementAsObject(cx, node, object);
      }
    }
  }

  if (aData.mXpath.WasPassed()) {
    for (auto& entry : aData.mXpath.Value().Entries()) {
      RefPtr<Element> node = FindNodeByXPath(aDocument, entry.mKey);
      if (node == nullptr) {
        continue;
      }
      if (entry.mValue.IsString()) {
        SetElementAsString(node, entry.mValue.GetAsString());
      } else if (entry.mValue.IsBoolean()) {
        SetElementAsBool(node, entry.mValue.GetAsBoolean());
      } else {
        JS::Rooted<JS::Value> object(
            aGlobal.Context(), JS::ObjectValue(*entry.mValue.GetAsObject()));
        SetElementAsObject(aGlobal.Context(), node, object);
      }
    }
  }

  return true;
}

MOZ_CAN_RUN_SCRIPT
void RestoreFormEntry(Element* aNode, const FormEntryValue& aValue) {
  using Type = sessionstore::FormEntryValue::Type;
  switch (aValue.type()) {
    case Type::TCheckbox:
      SetElementAsBool(aNode, aValue.get_Checkbox().value());
      break;
    case Type::TTextField:
      SetElementAsString(aNode, aValue.get_TextField().value());
      break;
    case Type::TFileList: {
      if (RefPtr<HTMLInputElement> input = HTMLInputElement::FromNode(aNode);
          input && input->ControlType() == FormControlType::InputFile) {
        CollectedFileListValue value;
        value.mFileList = aValue.get_FileList().valueList().Clone();
        SetElementAsFiles(input, value);
      }
      break;
    }
    case Type::TSingleSelect: {
      if (RefPtr<HTMLSelectElement> select = HTMLSelectElement::FromNode(aNode);
          select && !select->Multiple()) {
        CollectedNonMultipleSelectValue value;
        value.mSelectedIndex = aValue.get_SingleSelect().index();
        value.mValue = aValue.get_SingleSelect().value();
        SetElementAsSelect(select, value);
      }
      break;
    }
    case Type::TMultipleSelect: {
      if (RefPtr<HTMLSelectElement> select = HTMLSelectElement::FromNode(aNode);
          select && select->Multiple()) {
        SetElementAsMultiSelect(select,
                                aValue.get_MultipleSelect().valueList());
      }
      break;
    }
    case Type::TCustomElementTuple: {
      const auto* data = aNode->GetCustomElementData();
      if (!data || !data->IsFormAssociated()) {
        return;
      }
      auto* internals = data->GetElementInternals();
      nsCOMPtr<nsIGlobalObject> global = aNode->GetOwnerGlobal();
      internals->RestoreFormValue(
          nsContentUtils::ExtractFormAssociatedCustomElementValue(
              global, aValue.get_CustomElementTuple().value()),
          nsContentUtils::ExtractFormAssociatedCustomElementValue(
              global, aValue.get_CustomElementTuple().state()));
      break;
    }
    default:
      MOZ_ASSERT_UNREACHABLE();
  }
}

/* static */
void SessionStoreUtils::RestoreFormData(
    Document& aDocument, const nsString& aInnerHTML,
    const nsTArray<SessionStoreRestoreData::Entry>& aEntries) {
  using Change = SessionStoreChangeListener::Change;
  SessionStoreChangeListener::CollectSessionStoreData(
      aDocument.GetWindowContext(), EnumSet<Change>(Change::Input));

  if (!aInnerHTML.IsEmpty()) {
    SetInnerHTML(aDocument, aInnerHTML);
  }

  for (const auto& entry : aEntries) {
    RefPtr<Element> node = entry.mIsXPath
                               ? FindNodeByXPath(aDocument, entry.mData.id())
                               : aDocument.GetElementById(entry.mData.id());
    if (node) {
      RestoreFormEntry(node, entry.mData.value());
    }
  }
}

typedef void (*CollectorFunc)(JSContext* aCx, Document& aDocument,
                              Nullable<CollectedData>& aRetVal);

/**
 * A function that will recursively call |CollectorFunc| to collect data for all
 * non-dynamic frames in the current frame/docShell tree.
 */
static void CollectFrameTreeData(JSContext* aCx,
                                 BrowsingContext* aBrowsingContext,
                                 Nullable<CollectedData>& aRetVal,
                                 CollectorFunc aFunc) {
  if (aBrowsingContext->CreatedDynamically()) {
    return;
  }

  nsPIDOMWindowOuter* window = aBrowsingContext->GetDOMWindow();
  if (!window || !window->GetDocShell()) {
    return;
  }

  Document* document = window->GetExtantDoc();
  if (!document) {
    return;
  }

  /* Collect data from current frame */
  aFunc(aCx, *document, aRetVal);

  /* Collect data from all child frame */
  nsTArray<JSObject*> childrenData;
  SequenceRooter<JSObject*> rooter(aCx, &childrenData);
  uint32_t trailingNullCounter = 0;

  // This is not going to work for fission. Bug 1572084 for tracking it.
  for (auto& child : aBrowsingContext->Children()) {
    NullableRootedDictionary<CollectedData> data(aCx);
    CollectFrameTreeData(aCx, child, data, aFunc);
    if (data.IsNull()) {
      childrenData.AppendElement(nullptr);
      trailingNullCounter++;
      continue;
    }
    JS::Rooted<JS::Value> jsval(aCx);
    if (!ToJSValue(aCx, data.SetValue(), &jsval)) {
      JS_ClearPendingException(aCx);
      continue;
    }
    childrenData.AppendElement(&jsval.toObject());
    trailingNullCounter = 0;
  }

  if (trailingNullCounter != childrenData.Length()) {
    childrenData.TruncateLength(childrenData.Length() - trailingNullCounter);
    aRetVal.SetValue().mChildren.Construct() = std::move(childrenData);
  }
}

/* static */ void SessionStoreUtils::CollectScrollPosition(
    const GlobalObject& aGlobal, WindowProxyHolder& aWindow,
    Nullable<CollectedData>& aRetVal) {
  CollectFrameTreeData(aGlobal.Context(), aWindow.get(), aRetVal,
                       CollectCurrentScrollPosition);
}

/* static */ void SessionStoreUtils::CollectFormData(
    const GlobalObject& aGlobal, WindowProxyHolder& aWindow,
    Nullable<CollectedData>& aRetVal) {
  CollectFrameTreeData(aGlobal.Context(), aWindow.get(), aRetVal,
                       CollectCurrentFormData);
}

/* static */ void SessionStoreUtils::ComposeInputData(
    const nsTArray<CollectedInputDataValue>& aData, InputElementData& ret) {
  nsTArray<int> selectedIndex, valueIdx;
  nsTArray<nsString> id, selectVal, strVal, type;
  nsTArray<bool> boolVal;

  for (const CollectedInputDataValue& data : aData) {
    id.AppendElement(data.id);
    type.AppendElement(data.type);

    if (data.value.is<mozilla::dom::CollectedNonMultipleSelectValue>()) {
      valueIdx.AppendElement(selectVal.Length());
      selectedIndex.AppendElement(
          data.value.as<mozilla::dom::CollectedNonMultipleSelectValue>()
              .mSelectedIndex);
      selectVal.AppendElement(
          data.value.as<mozilla::dom::CollectedNonMultipleSelectValue>()
              .mValue);
    } else if (data.value.is<CopyableTArray<nsString>>()) {
      // The first valueIdx is "index of the first string value"
      valueIdx.AppendElement(strVal.Length());
      strVal.AppendElements(data.value.as<CopyableTArray<nsString>>());
      // The second valueIdx is "index of the last string value" + 1
      id.AppendElement(data.id);
      type.AppendElement(data.type);
      valueIdx.AppendElement(strVal.Length());
    } else if (data.value.is<nsString>()) {
      valueIdx.AppendElement(strVal.Length());
      strVal.AppendElement(data.value.as<nsString>());
    } else if (data.type.EqualsLiteral("bool")) {
      valueIdx.AppendElement(boolVal.Length());
      boolVal.AppendElement(data.value.as<bool>());
    }
  }

  if (selectedIndex.Length() != 0) {
    ret.mSelectedIndex.Construct(std::move(selectedIndex));
  }
  if (valueIdx.Length() != 0) {
    ret.mValueIdx.Construct(std::move(valueIdx));
  }
  if (id.Length() != 0) {
    ret.mId.Construct(std::move(id));
  }
  if (selectVal.Length() != 0) {
    ret.mSelectVal.Construct(std::move(selectVal));
  }
  if (strVal.Length() != 0) {
    ret.mStrVal.Construct(std::move(strVal));
  }
  if (type.Length() != 0) {
    ret.mType.Construct(std::move(type));
  }
  if (boolVal.Length() != 0) {
    ret.mBoolVal.Construct(std::move(boolVal));
  }
}

already_AddRefed<nsISessionStoreRestoreData>
SessionStoreUtils::ConstructSessionStoreRestoreData(
    const GlobalObject& aGlobal) {
  nsCOMPtr<nsISessionStoreRestoreData> data = new SessionStoreRestoreData();
  return data.forget();
}

/* static */
already_AddRefed<Promise> SessionStoreUtils::InitializeRestore(
    const GlobalObject& aGlobal, CanonicalBrowsingContext& aContext,
    nsISessionStoreRestoreData* aData, ErrorResult& aError) {
  if (!mozilla::SessionHistoryInParent()) {
    MOZ_CRASH("why were we called?");
  }

  MOZ_DIAGNOSTIC_ASSERT(aContext.IsTop());

  MOZ_DIAGNOSTIC_ASSERT(aData);
  nsCOMPtr<SessionStoreRestoreData> data = do_QueryInterface(aData);
  aContext.SetRestoreData(data, aError);
  if (aError.Failed()) {
    return nullptr;
  }

  MOZ_DIAGNOSTIC_ASSERT(aContext.GetSessionHistory());
  aContext.GetSessionHistory()->ReloadCurrentEntry();

  return aContext.GetRestorePromise();
}

/* static */
void SessionStoreUtils::RestoreDocShellState(
    nsIDocShell* aDocShell, const DocShellRestoreState& aState) {
  if (aDocShell) {
    if (aState.URI()) {
      aDocShell->SetCurrentURIForSessionStore(aState.URI());
    }
    RestoreDocShellCapabilities(aDocShell, aState.docShellCaps());
  }
}

/* static */
already_AddRefed<Promise> SessionStoreUtils::RestoreDocShellState(
    const GlobalObject& aGlobal, CanonicalBrowsingContext& aContext,
    const nsACString& aURL, const nsCString& aDocShellCaps,
    ErrorResult& aError) {
  MOZ_RELEASE_ASSERT(mozilla::SessionHistoryInParent());
  MOZ_RELEASE_ASSERT(aContext.IsTop());

  WindowGlobalParent* wgp = aContext.GetCurrentWindowGlobal();
  if (!wgp) {
    aError.Throw(NS_ERROR_FAILURE);
    return nullptr;
  }

  nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
  MOZ_DIAGNOSTIC_ASSERT(global);

  RefPtr<Promise> promise = Promise::Create(global, aError);
  if (aError.Failed()) {
    return nullptr;
  }

  nsCOMPtr<nsIURI> uri;
  if (!aURL.IsEmpty()) {
    if (NS_FAILED(NS_NewURI(getter_AddRefs(uri), aURL))) {
      aError.Throw(NS_ERROR_FAILURE);
      return nullptr;
    }
  }

  bool allowJavascript = true;
  for (const nsACString& token :
       nsCCharSeparatedTokenizer(aDocShellCaps, ',').ToRange()) {
    if (token.EqualsLiteral("Javascript")) {
      allowJavascript = false;
    }
  }

  Unused << aContext.SetAllowJavascript(allowJavascript);

  DocShellRestoreState state = {uri, aDocShellCaps};

  // TODO (anny): Investigate removing this roundtrip.
  wgp->SendRestoreDocShellState(state)->Then(
      GetMainThreadSerialEventTarget(), __func__,
      [promise](void) { promise->MaybeResolveWithUndefined(); },
      [promise](void) { promise->MaybeRejectWithUndefined(); });

  return promise.forget();
}

/* static */
void SessionStoreUtils::RestoreSessionStorageFromParent(
    const GlobalObject& aGlobal, const CanonicalBrowsingContext& aContext,
    const Record<nsCString, Record<nsString, nsString>>& aSessionStorage) {
  nsTArray<SSCacheCopy> cacheInitList;
  for (const auto& originEntry : aSessionStorage.Entries()) {
    nsCOMPtr<nsIPrincipal> storagePrincipal =
        BasePrincipal::CreateContentPrincipal(originEntry.mKey);

    nsCString originKey;
    nsresult rv = storagePrincipal->GetStorageOriginKey(originKey);
    if (NS_FAILED(rv)) {
      continue;
    }

    SSCacheCopy& cacheInit = *cacheInitList.AppendElement();

    cacheInit.originKey() = originKey;
    PrincipalToPrincipalInfo(storagePrincipal, &cacheInit.principalInfo());

    for (const auto& entry : originEntry.mValue.Entries()) {
      SSSetItemInfo& setItemInfo = *cacheInit.data().AppendElement();
      setItemInfo.key() = entry.mKey;
      setItemInfo.value() = entry.mValue;
    }
  }

  BackgroundSessionStorageManager::LoadData(aContext.Id(), cacheInitList);
}

/* static */
nsresult SessionStoreUtils::ConstructFormDataValues(
    JSContext* aCx, const nsTArray<sessionstore::FormEntry>& aValues,
    nsTArray<Record<nsString, OwningStringOrBooleanOrObject>::EntryType>&
        aEntries,
    bool aParseSessionData) {
  using EntryType = Record<nsString, OwningStringOrBooleanOrObject>::EntryType;

  if (!aEntries.SetCapacity(aValues.Length(), fallible)) {
    return NS_ERROR_FAILURE;
  }

  for (const auto& value : aValues) {
    EntryType* entry = aEntries.AppendElement();

    using Type = sessionstore::FormEntryValue::Type;
    switch (value.value().type()) {
      case Type::TCheckbox:
        entry->mValue.SetAsBoolean() = value.value().get_Checkbox().value();
        break;
      case Type::TTextField: {
        if (aParseSessionData && value.id() == u"sessionData"_ns) {
          JS::Rooted<JS::Value> jsval(aCx);
          const auto& fieldValue = value.value().get_TextField().value();
          if (!JS_ParseJSON(aCx, fieldValue.get(), fieldValue.Length(),
                            &jsval) ||
              !jsval.isObject()) {
            return NS_ERROR_FAILURE;
          }
          entry->mValue.SetAsObject() = &jsval.toObject();
        } else {
          entry->mValue.SetAsString() = value.value().get_TextField().value();
        }
        break;
      }
      case Type::TFileList: {
        CollectedFileListValue file;
        file.mFileList = value.value().get_FileList().valueList().Clone();

        JS::Rooted<JS::Value> jsval(aCx);
        if (!ToJSValue(aCx, file, &jsval) || !jsval.isObject()) {
          return NS_ERROR_FAILURE;
        }
        entry->mValue.SetAsObject() = &jsval.toObject();
        break;
      }
      case Type::TSingleSelect: {
        CollectedNonMultipleSelectValue select;
        select.mSelectedIndex = value.value().get_SingleSelect().index();
        select.mValue = value.value().get_SingleSelect().value();

        JS::Rooted<JS::Value> jsval(aCx);
        if (!ToJSValue(aCx, select, &jsval) || !jsval.isObject()) {
          return NS_ERROR_FAILURE;
        }
        entry->mValue.SetAsObject() = &jsval.toObject();
        break;
      }
      case Type::TMultipleSelect: {
        JS::Rooted<JS::Value> jsval(aCx);
        if (!ToJSValue(aCx, value.value().get_MultipleSelect().valueList(),
                       &jsval) ||
            !jsval.isObject()) {
          return NS_ERROR_FAILURE;
        }
        entry->mValue.SetAsObject() = &jsval.toObject();
        break;
      }
      case Type::TCustomElementTuple: {
        nsCOMPtr<nsIGlobalObject> global;
        JS::Rooted<JSObject*> globalObject(aCx, JS::CurrentGlobalOrNull(aCx));
        if (NS_WARN_IF(!globalObject)) {
          break;
        }
        global = xpc::NativeGlobal(globalObject);
        if (NS_WARN_IF(!global)) {
          break;
        }

        auto formState =
            nsContentUtils::ExtractFormAssociatedCustomElementValue(
                global, value.value().get_CustomElementTuple().state());
        auto formValue =
            nsContentUtils::ExtractFormAssociatedCustomElementValue(
                global, value.value().get_CustomElementTuple().value());
        MOZ_ASSERT(!formValue.IsNull() || !formState.IsNull(),
                   "Shouldn't be storing null values!");

        CollectedCustomElementValue val;
        val.mValue = formValue;
        val.mState = formState;
        JS::Rooted<JS::Value> jsval(aCx);
        if (!ToJSValue(aCx, val, &jsval)) {
          return NS_ERROR_FAILURE;
        }
        entry->mValue.SetAsObject() = &jsval.toObject();
        break;
      }
      default:
        break;
    }

    entry->mKey = value.id();
  }

  return NS_OK;
}

static nsresult ConstructSessionStorageValue(
    const nsTArray<SSSetItemInfo>& aValues,
    Record<nsString, nsString>& aRecord) {
  auto& entries = aRecord.Entries();
  for (const auto& value : aValues) {
    auto entry = entries.AppendElement();
    entry->mKey = value.key();
    entry->mValue = value.value();
  }

  return NS_OK;
}

/* static */
nsresult SessionStoreUtils::ConstructSessionStorageValues(
    CanonicalBrowsingContext* aBrowsingContext,
    const nsTArray<SSCacheCopy>& aValues,
    Record<nsCString, Record<nsString, nsString>>& aRecord) {
  if (!aRecord.Entries().SetCapacity(aValues.Length(), fallible)) {
    return NS_ERROR_FAILURE;
  }

  for (const auto& value : aValues) {
    auto storagePrincipal = PrincipalInfoToPrincipal(value.principalInfo());
    if (storagePrincipal.isErr()) {
      continue;
    }

    auto entry = aRecord.Entries().AppendElement();

    if (!entry->mValue.Entries().SetCapacity(value.data().Length(), fallible)) {
      return NS_ERROR_FAILURE;
    }

    if (NS_FAILED(storagePrincipal.inspect()->GetOrigin(entry->mKey))) {
      return NS_ERROR_FAILURE;
    }

    ConstructSessionStorageValue(value.data(), entry->mValue);
  }

  return NS_OK;
}

/* static */
bool SessionStoreUtils::CopyProperty(JSContext* aCx, JS::Handle<JSObject*> aDst,
                                     JS::Handle<JSObject*> aSrc,
                                     const nsAString& aName) {
  JS::Rooted<JS::PropertyKey> name(aCx);
  const char16_t* data;
  size_t length = aName.GetData(&data);

  if (!JS_CharsToId(aCx, JS::TwoByteChars(data, length), &name)) {
    return false;
  }

  bool found = false;
  if (!JS_HasPropertyById(aCx, aSrc, name, &found) || !found) {
    return true;
  }

  JS::Rooted<JS::Value> value(aCx);
  if (!JS_GetPropertyById(aCx, aSrc, name, &value)) {
    return false;
  }

  if (value.isNullOrUndefined()) {
    return true;
  }

  return JS_DefinePropertyById(aCx, aDst, name, value, JSPROP_ENUMERATE);
}
