/* 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 "jsapi.h" #include "mozilla/PresShell.h" #include "mozilla/dom/AutocompleteInfoBinding.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/DocumentInlines.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/SessionStoreUtils.h" #include "mozilla/dom/txIXPathContext.h" #include "mozilla/dom/WindowProxyHolder.h" #include "mozilla/dom/XPathResult.h" #include "mozilla/dom/XPathEvaluator.h" #include "mozilla/dom/XPathExpression.h" #include "mozilla/UniquePtr.h" #include "nsCharSeparatedTokenizer.h" #include "nsContentList.h" #include "nsContentUtils.h" #include "nsFocusManager.h" #include "nsGlobalWindowOuter.h" #include "nsIDocShell.h" #include "nsIFormControl.h" #include "nsIScrollableFrame.h" #include "nsPresContext.h" #include "nsPrintfCString.h" using namespace mozilla; using namespace mozilla::dom; 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 context = outer->GetBrowsingContext(); return context && !context->CreatedDynamically(); } RefPtr 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 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 item; docShell->GetInProcessChildAt(i, getter_AddRefs(item)); if (!item) { aRv.Throw(NS_ERROR_FAILURE); return; } RefPtr context = item->GetBrowsingContext(); if (!context) { aRv.Throw(NS_ERROR_FAILURE); return; } if (context->CreatedDynamically()) { continue; } nsCOMPtr childDocShell(do_QueryInterface(item)); if (!childDocShell) { aRv.Throw(NS_ERROR_FAILURE); return; } int32_t childOffset = childDocShell->GetChildOffset(); aCallback.Call(WindowProxyHolder(context.forget()), childOffset); } } /* static */ already_AddRefed SessionStoreUtils::AddDynamicFrameFilteredListener( const GlobalObject& aGlobal, EventTarget& aTarget, const nsAString& aType, JS::Handle 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 obj(cx, &aListener.toObject()); JS::Rooted global(cx, JS::CurrentGlobalOrNull(cx)); RefPtr listener = new EventListener(cx, obj, global, GetIncumbentGlobal()); nsCOMPtr 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 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( const GlobalObject& aGlobal, nsIDocShell* aDocShell, const nsCString& aDisallowCapabilities) { aDocShell->SetAllowPlugins(true); aDocShell->SetAllowJavascript(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); for (const nsACString& token : nsCCharSeparatedTokenizer(aDisallowCapabilities, ',').ToRange()) { if (token.EqualsLiteral("Plugins")) { aDocShell->SetAllowPlugins(false); } else if (token.EqualsLiteral("Javascript")) { aDocShell->SetAllowJavascript(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); } } } static void CollectCurrentScrollPosition(JSContext* aCx, Document& aDocument, Nullable& 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()) { return; } nsCCharSeparatedTokenizer tokenizer(aData.mScroll.Value(), ','); 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 doc = aWindow.GetExtantDoc()) { if (nsPresContext* presContext = doc->GetPresContext()) { if (presContext->IsRootContentDocument()) { // 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::EntryType* AppendEntryToCollectedData(nsINode* aNode, const nsAString& aId, uint16_t& aGeneratedCount, Nullable& aRetVal) { Record::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; } // A helper function to append a element into aXPathVals or aIdVals static void AppendEntryToCollectedData( nsINode* aNode, const nsAString& aId, CollectedInputDataValue& aEntry, uint16_t& aNumXPath, uint16_t& aNumId, nsTArray& aXPathVals, nsTArray& aIdVals) { if (!aId.IsEmpty()) { aEntry.id = aId; aIdVals.AppendElement(aEntry); aNumId++; } else { nsAutoString xpath; aNode->GenerateXPath(xpath); aEntry.id = xpath; aXPathVals.AppendElement(aEntry); aNumXPath++; } } /* for bool value */ static void AppendValueToCollectedData(nsINode* aNode, const nsAString& aId, const bool& aValue, uint16_t& aGeneratedCount, JSContext* aCx, Nullable& aRetVal) { Record::EntryType* entry = AppendEntryToCollectedData(aNode, aId, aGeneratedCount, aRetVal); entry->mValue.SetAsBoolean() = aValue; } /* for bool value */ static void AppendValueToCollectedData( nsINode* aNode, const nsAString& aId, const bool& aValue, uint16_t& aNumXPath, uint16_t& aNumId, nsTArray& aXPathVals, nsTArray& aIdVals) { CollectedInputDataValue entry; entry.type = u"bool"_ns; entry.value = AsVariant(aValue); AppendEntryToCollectedData(aNode, aId, entry, aNumXPath, aNumId, aXPathVals, aIdVals); } /* for nsString value */ static void AppendValueToCollectedData(nsINode* aNode, const nsAString& aId, const nsString& aValue, uint16_t& aGeneratedCount, Nullable& aRetVal) { Record::EntryType* entry = AppendEntryToCollectedData(aNode, aId, aGeneratedCount, aRetVal); entry->mValue.SetAsString() = aValue; } /* for nsString value */ static void AppendValueToCollectedData( nsINode* aNode, const nsAString& aId, const nsString& aValue, uint16_t& aNumXPath, uint16_t& aNumId, nsTArray& aXPathVals, nsTArray& aIdVals) { CollectedInputDataValue entry; entry.type = u"string"_ns; entry.value = AsVariant(aValue); AppendEntryToCollectedData(aNode, aId, entry, aNumXPath, aNumId, aXPathVals, aIdVals); } /* for single select value */ static void AppendValueToCollectedData( nsINode* aNode, const nsAString& aId, const CollectedNonMultipleSelectValue& aValue, uint16_t& aGeneratedCount, JSContext* aCx, Nullable& aRetVal) { JS::Rooted jsval(aCx); if (!ToJSValue(aCx, aValue, &jsval)) { JS_ClearPendingException(aCx); return; } Record::EntryType* entry = AppendEntryToCollectedData(aNode, aId, aGeneratedCount, aRetVal); entry->mValue.SetAsObject() = &jsval.toObject(); } /* for single select value */ static void AppendValueToCollectedData( nsINode* aNode, const nsAString& aId, const CollectedNonMultipleSelectValue& aValue, uint16_t& aNumXPath, uint16_t& aNumId, nsTArray& aXPathVals, nsTArray& aIdVals) { CollectedInputDataValue entry; entry.type = u"singleSelect"_ns; entry.value = AsVariant(aValue); AppendEntryToCollectedData(aNode, aId, entry, aNumXPath, aNumId, aXPathVals, aIdVals); } /* 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& 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 jsval(aCx); if (JS_ParseJSON(aCx, aValue.get(), aValue.Length(), &jsval) && jsval.isObject()) { Record::EntryType* entry = AppendEntryToCollectedData(aNode, aId, aGeneratedCount, aRetVal); entry->mValue.SetAsObject() = &jsval.toObject(); } else { JS_ClearPendingException(aCx); } return; } } } AppendValueToCollectedData(aNode, aId, aValue, aGeneratedCount, aRetVal); } static void AppendValueToCollectedData( Document& aDocument, nsINode* aNode, const nsAString& aId, const nsString& aValue, uint16_t& aNumXPath, uint16_t& aNumId, nsTArray& aXPathVals, nsTArray& aIdVals) { CollectedInputDataValue entry; entry.type = u"string"_ns; entry.value = AsVariant(aValue); AppendEntryToCollectedData(aNode, aId, entry, aNumXPath, aNumId, aXPathVals, aIdVals); } /* for nsTArray: file and multipleSelect */ static void AppendValueToCollectedData(nsINode* aNode, const nsAString& aId, const nsAString& aValueType, nsTArray& aValue, uint16_t& aGeneratedCount, JSContext* aCx, Nullable& aRetVal) { JS::Rooted 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::EntryType* entry = AppendEntryToCollectedData(aNode, aId, aGeneratedCount, aRetVal); entry->mValue.SetAsObject() = &jsval.toObject(); } /* for nsTArray: file and multipleSelect */ static void AppendValueToCollectedData( nsINode* aNode, const nsAString& aId, const nsAString& aValueType, const nsTArray& aValue, uint16_t& aNumXPath, uint16_t& aNumId, nsTArray& aXPathVals, nsTArray& aIdVals) { CollectedInputDataValue entry; entry.type = aValueType; entry.value = AsVariant(CopyableTArray(aValue.Clone())); AppendEntryToCollectedData(aNode, aId, entry, aNumXPath, aNumId, aXPathVals, aIdVals); } /* static */ template void SessionStoreUtils::CollectFromTextAreaElement(Document& aDocument, uint16_t& aGeneratedCount, ArgsT&&... args) { RefPtr 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(args)...); } } /* static */ template void SessionStoreUtils::CollectFromInputElement(Document& aDocument, uint16_t& aGeneratedCount, ArgsT&&... args) { RefPtr 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 formControl = do_QueryInterface(inputlist->Item(i)); if (formControl) { uint8_t controlType = formControl->ControlType(); if (controlType == NS_FORM_INPUT_PASSWORD || controlType == NS_FORM_INPUT_HIDDEN || controlType == NS_FORM_INPUT_BUTTON || controlType == NS_FORM_INPUT_IMAGE || controlType == NS_FORM_INPUT_SUBMIT || controlType == NS_FORM_INPUT_RESET) { continue; } } RefPtr input = HTMLInputElement::FromNodeOrNull(inputlist->Item(i)); if (!input || !nsContentUtils::IsAutocompleteEnabled(input)) { continue; } nsAutoString id; input->GetId(id); if (id.IsEmpty() && (aGeneratedCount > kMaxTraversedXPaths)) { continue; } Nullable aInfo; input->GetAutocompleteInfo(aInfo); if (!aInfo.IsNull() && !aInfo.Value().mCanAutomaticallyPersist) { continue; } if (input->ControlType() == NS_FORM_INPUT_CHECKBOX || input->ControlType() == NS_FORM_INPUT_RADIO) { bool checked = input->Checked(); if (checked == input->DefaultChecked()) { continue; } AppendValueToCollectedData(input, id, checked, aGeneratedCount, std::forward(args)...); } else if (input->ControlType() == NS_FORM_INPUT_FILE) { IgnoredErrorResult rv; nsTArray result; input->MozGetFileNameArray(result, rv); if (rv.Failed() || result.Length() == 0) { continue; } AppendValueToCollectedData(input, id, u"file"_ns, result, aGeneratedCount, std::forward(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(args)...); } } } /* static */ template void SessionStoreUtils::CollectFromSelectElement(Document& aDocument, uint16_t& aGeneratedCount, ArgsT&&... args) { RefPtr 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 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()) { // s with the multiple attribute are easier to determine the // default value since each