diff options
Diffstat (limited to 'toolkit/components/sessionstore/SessionStoreUtils.cpp')
-rw-r--r-- | toolkit/components/sessionstore/SessionStoreUtils.cpp | 1948 |
1 files changed, 1948 insertions, 0 deletions
diff --git a/toolkit/components/sessionstore/SessionStoreUtils.cpp b/toolkit/components/sessionstore/SessionStoreUtils.cpp new file mode 100644 index 0000000000..3c94a54261 --- /dev/null +++ b/toolkit/components/sessionstore/SessionStoreUtils.cpp @@ -0,0 +1,1948 @@ +/* -*- 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 "nsGlobalWindowInner.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" +#include "nsDocShell.h" +#include "nsNetUtil.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 + + // 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->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("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; + } + + nsCOMPtr<nsISHistory> shistory = aContext.GetSessionHistory(); + MOZ_DIAGNOSTIC_ASSERT(shistory); + shistory->ReloadCurrentEntry(); + + return aContext.GetRestorePromise(); +} + +/* static */ +void SessionStoreUtils::RestoreDocShellState( + nsIDocShell* aDocShell, const DocShellRestoreState& aState) { + if (aDocShell) { + nsCOMPtr<nsIURI> currentUri; + nsDocShell::Cast(aDocShell)->GetCurrentURI(getter_AddRefs(currentUri)); + if (aState.URI() && + (!currentUri || mozilla::net::SchemeIsAbout(currentUri))) { + 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); +} |