diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /toolkit/components/sessionstore | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/sessionstore')
-rw-r--r-- | toolkit/components/sessionstore/SessionStoreData.h | 61 | ||||
-rw-r--r-- | toolkit/components/sessionstore/SessionStoreFunctions.idl | 19 | ||||
-rw-r--r-- | toolkit/components/sessionstore/SessionStoreFunctions.jsm | 456 | ||||
-rw-r--r-- | toolkit/components/sessionstore/SessionStoreListener.cpp | 852 | ||||
-rw-r--r-- | toolkit/components/sessionstore/SessionStoreListener.h | 174 | ||||
-rw-r--r-- | toolkit/components/sessionstore/SessionStoreMessageUtils.h | 71 | ||||
-rw-r--r-- | toolkit/components/sessionstore/SessionStoreUtils.cpp | 1366 | ||||
-rw-r--r-- | toolkit/components/sessionstore/SessionStoreUtils.h | 101 | ||||
-rw-r--r-- | toolkit/components/sessionstore/moz.build | 34 |
9 files changed, 3134 insertions, 0 deletions
diff --git a/toolkit/components/sessionstore/SessionStoreData.h b/toolkit/components/sessionstore/SessionStoreData.h new file mode 100644 index 0000000000..0df1ea5bf5 --- /dev/null +++ b/toolkit/components/sessionstore/SessionStoreData.h @@ -0,0 +1,61 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef mozilla_dom_SessionStoreData_h +#define mozilla_dom_SessionStoreData_h + +#include "nsString.h" +#include "nsTArray.h" +#include "mozilla/dom/SessionStoreUtilsBinding.h" +#include "mozilla/Variant.h" + +typedef mozilla::Variant<nsString, bool, + mozilla::dom::CollectedNonMultipleSelectValue, + CopyableTArray<nsString>> + InputDataValue; + +/* + * Need two arrays based on this struct. + * One is for elements with id one is for XPath. + * + * id: id or XPath + * type: type of this input element + * bool: value is boolean + * string: value is nsString + * file: value is "arrayVal" + * singleSelect: value is "singleSelect" + * multipleSelect: value is "arrayVal" + * + * There are four value types: + * strVal: nsString + * boolVal: boolean + * singleSelect: single select value + * arrayVal: nsString array + */ +struct CollectedInputDataValue { + nsString id; + nsString type; + InputDataValue value{false}; + + CollectedInputDataValue() = default; +}; + +/* + * Each index of the following array is corresponging to each frame. + * descendants: number of child frames of this frame + * innerHTML: innerHTML of this frame + * url: url of this frame + * numId: number of containing elements with id for this frame + * numXPath: number of containing elements with XPath for this frame + */ +struct InputFormData { + int32_t descendants; + nsString innerHTML; + nsCString url; + int32_t numId; + int32_t numXPath; +}; + +#endif /* mozilla_dom_SessionStoreData_h */ diff --git a/toolkit/components/sessionstore/SessionStoreFunctions.idl b/toolkit/components/sessionstore/SessionStoreFunctions.idl new file mode 100644 index 0000000000..1fd9bfb22d --- /dev/null +++ b/toolkit/components/sessionstore/SessionStoreFunctions.idl @@ -0,0 +1,19 @@ +/* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 8 -*- */ +/* 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 "nsISupports.idl" + +webidl BrowsingContext; +webidl Element; + +[scriptable, uuid(1A060FBA-A19D-11E9-B7EB-580D0EDD8E6F)] +interface nsISessionStoreFunctions : nsISupports { + // update sessionStore from the tabListener implemented by C++ + // aData is a UpdateSessionStoreData dictionary (From SessionStoreUtils.webidl) + void UpdateSessionStore( + in Element aBrowser, in BrowsingContext aBrowsingContext, + in uint32_t aFlushId, in boolean aIsFinal, in uint32_t aEpoch, + in jsval aData, in boolean aCollectSHistory); +}; diff --git a/toolkit/components/sessionstore/SessionStoreFunctions.jsm b/toolkit/components/sessionstore/SessionStoreFunctions.jsm new file mode 100644 index 0000000000..fc5f73651a --- /dev/null +++ b/toolkit/components/sessionstore/SessionStoreFunctions.jsm @@ -0,0 +1,456 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * 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/. */ +ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm", this); + +XPCOMUtils.defineLazyModuleGetters(this, { + SessionStore: "resource:///modules/sessionstore/SessionStore.jsm", +}); + +function UpdateSessionStore( + aBrowser, + aBrowsingContext, + aFlushId, + aIsFinal, + aEpoch, + aData, + aCollectSHistory +) { + return SessionStoreFuncInternal.updateSessionStore( + aBrowser, + aBrowsingContext, + aFlushId, + aIsFinal, + aEpoch, + aData, + aCollectSHistory + ); +} + +var EXPORTED_SYMBOLS = ["UpdateSessionStore"]; + +var SessionStoreFuncInternal = { + // form data which is waiting to be updated + _formDataId: [], + _formDataIdValue: [], + _formDataXPath: [], + _formDataXPathValue: [], + + /** + * The data will be stored in the arrays: + * "_formDataId, _formDataIdValue" for the elements with id. + * "_formDataXPath, _formDataXPathValue" for the elements with XPath. + */ + updateFormData: function SSF_updateFormData(aType, aData) { + let idArray = this._formDataId; + let valueArray = this._formDataIdValue; + + if (aType == "XPath") { + idArray = this._formDataXPath; + valueArray = this._formDataXPathValue; + } + + let valueIdx = aData.valueIdx; + for (let i = 0; i < aData.id.length; i++) { + idArray.push(aData.id[i]); + + if (aData.type[i] == "singleSelect") { + valueArray.push({ + selectedIndex: aData.selectedIndex[valueIdx[i]], + value: aData.selectVal[valueIdx[i]], + }); + } else if (aData.type[i] == "file") { + valueArray.push({ + type: "file", + fileList: aData.strVal.slice(valueIdx[i], valueIdx[++i]), + }); + } else if (aData.type[i] == "multipleSelect") { + valueArray.push(aData.strVal.slice(valueIdx[i], valueIdx[++i])); + } else if (aData.type[i] == "string") { + valueArray.push(aData.strVal[valueIdx[i]]); + } else if (aData.type[i] == "bool") { + valueArray.push(aData.boolVal[valueIdx[i]]); + } + } + }, + + /** + * Return the array of formdata for this._sessionData.formdata.children + * + * aStartIndex: Current index for aInnerHTML/aUrl/aNumId/aNumXPath/aDescendants. + * (aStartIndex means the index of current root frame) + * aInnerHTML: Array for innerHTML. + * aUrl: Array for url. + * aNumId: Array for number of containing elements with id + * aNumXPath: Array for number of containing elements with XPath + * aDescendants: Array for number of descendants. + * + * aCurrentIdIdx: Current index for this._formDataId and this._formDataIdValue + * aCurrentXPathIdx: Current index for this._formDataXPath and this._formDataXPathValue + * aNumberOfDescendants: The number of descendants for current frame + * + * The returned array includes "aNumberOfDescendants" formdata objects. + */ + composeInputChildren: function SSF_composeInputChildren( + aInnerHTML, + aUrl, + aCurrentIdIdx, + aNumId, + aCurrentXpathIdx, + aNumXPath, + aDescendants, + aStartIndex, + aNumberOfDescendants + ) { + let children = []; + let lastIndexOfNonNullbject = -1; + for (let i = 0; i < aNumberOfDescendants; i++) { + let currentIndex = aStartIndex + i; + let obj = {}; + let objWithData = false; + + // set url/id/xpath + if (aUrl[currentIndex]) { + obj.url = aUrl[currentIndex]; + objWithData = true; + + if (aInnerHTML[currentIndex]) { + // eslint-disable-next-line no-unsanitized/property + obj.innerHTML = aInnerHTML[currentIndex]; + } + if (aNumId[currentIndex]) { + let idObj = {}; + for (let idx = 0; idx < aNumId[currentIndex]; idx++) { + idObj[ + this._formDataId[aCurrentIdIdx + idx] + ] = this._formDataIdValue[aCurrentIdIdx + idx]; + } + obj.id = idObj; + } + + // 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 ( + obj.url == "about:sessionrestore" || + obj.url == "about:welcomeback" + ) { + obj.id.sessionData = JSON.parse(obj.id.sessionData); + } + + if (aNumXPath[currentIndex]) { + let xpathObj = {}; + for (let idx = 0; idx < aNumXPath[currentIndex]; idx++) { + xpathObj[ + this._formDataXPath[aCurrentXpathIdx + idx] + ] = this._formDataXPathValue[aCurrentXpathIdx + idx]; + } + obj.xpath = xpathObj; + } + } + + // compose the descendantsTree which will be pushed into children array + if (aDescendants[currentIndex]) { + let descendantsTree = this.composeInputChildren( + aInnerHTML, + aUrl, + aCurrentIdIdx + aNumId[currentIndex], + aNumId, + aCurrentXpathIdx + aNumXPath[currentIndex], + aNumXPath, + aDescendants, + currentIndex + 1, + aDescendants[currentIndex] + ); + i += aDescendants[currentIndex]; + if (descendantsTree) { + obj.children = descendantsTree; + } + } + + if (objWithData) { + lastIndexOfNonNullbject = children.length; + children.push(obj); + } else { + children.push(null); + } + } + + if (lastIndexOfNonNullbject == -1) { + return null; + } + + return children.slice(0, lastIndexOfNonNullbject + 1); + }, + + /** + * Update the object for this._sessionData.formdata. + * The object contains the formdata for all reachable frames. + * + * "object.children" is an array with one entry per frame, + * containing formdata as a nested data structure according + * to the layout of the frame tree, or null if no formdata. + * + * Example: + * { + * url: "http://mozilla.org/", + * id: {input_id: "input value"}, + * xpath: {input_xpath: "input value"}, + * children: [ + * null, + * {url: "http://sub.mozilla.org/", id: {input_id: "input value 2"}} + * ] + * } + * + * Each index of the following array is corresponging to each frame. + * aDescendants: Array for number of descendants + * aInnerHTML: Array for innerHTML + * aUrl: Array for url + * aNumId: Array for number of containing elements with id + * aNumXPath: Array for number of containing elements with XPath + * + * Here we use [index 0] to compose the formdata object of root frame. + * Besides, we use composeInputChildren() to get array of "object.children". + */ + updateInput: function SSF_updateInput( + aSessionData, + aDescendants, + aInnerHTML, + aUrl, + aNumId, + aNumXPath + ) { + let obj = {}; + let objWithData = false; + + if (aUrl[0]) { + obj.url = aUrl[0]; + + if (aInnerHTML[0]) { + // eslint-disable-next-line no-unsanitized/property + obj.innerHTML = aInnerHTML[0]; + objWithData = true; + } + + if (aNumId[0]) { + let idObj = {}; + for (let i = 0; i < aNumId[0]; i++) { + idObj[this._formDataId[i]] = this._formDataIdValue[i]; + } + obj.id = idObj; + objWithData = true; + } + + // 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 (obj.url == "about:sessionrestore" || obj.url == "about:welcomeback") { + obj.id.sessionData = JSON.parse(obj.id.sessionData); + } + + if (aNumXPath[0]) { + let xpathObj = {}; + for (let i = 0; i < aNumXPath[0]; i++) { + xpathObj[this._formDataXPath[i]] = this._formDataXPathValue[i]; + } + obj.xpath = xpathObj; + objWithData = true; + } + } + + if (aDescendants.length > 1) { + let descendantsTree = this.composeInputChildren( + aInnerHTML, + aUrl, + aNumId[0], + aNumId, + aNumXPath[0], + aNumXPath, + aDescendants, + 1, + aDescendants[0] + ); + if (descendantsTree) { + obj.children = descendantsTree; + objWithData = true; + } + } + + if (objWithData) { + aSessionData.formdata = obj; + } else { + aSessionData.formdata = null; + } + }, + + composeChildren: function SSF_composeScrollPositionsData( + aPositions, + aDescendants, + aStartIndex, + aNumberOfDescendants + ) { + let children = []; + let lastIndexOfNonNullbject = -1; + for (let i = 0; i < aNumberOfDescendants; i++) { + let currentIndex = aStartIndex + i; + let obj = {}; + let objWithData = false; + if (aPositions[currentIndex]) { + obj.scroll = aPositions[currentIndex]; + objWithData = true; + } + if (aDescendants[currentIndex]) { + let descendantsTree = this.composeChildren( + aPositions, + aDescendants, + currentIndex + 1, + aDescendants[currentIndex] + ); + i += aDescendants[currentIndex]; + if (descendantsTree) { + obj.children = descendantsTree; + objWithData = true; + } + } + + if (objWithData) { + lastIndexOfNonNullbject = children.length; + children.push(obj); + } else { + children.push(null); + } + } + + if (lastIndexOfNonNullbject == -1) { + return null; + } + + return children.slice(0, lastIndexOfNonNullbject + 1); + }, + + updateScrollPositions: function SSF_updateScrollPositions( + aPositions, + aDescendants + ) { + let obj = {}; + let objWithData = false; + + if (aPositions[0]) { + obj.scroll = aPositions[0]; + objWithData = true; + } + + if (aPositions.length > 1) { + let children = this.composeChildren( + aPositions, + aDescendants, + 1, + aDescendants[0] + ); + if (children) { + obj.children = children; + objWithData = true; + } + } + if (objWithData) { + return obj; + } + return null; + }, + + updateStorage: function SSF_updateStorage(aOrigins, aKeys, aValues) { + let data = {}; + for (let i = 0; i < aOrigins.length; i++) { + // If the key isn't defined, then .clear() was called, and we send + // up null for this domain to indicate that storage has been cleared + // for it. + if (aKeys[i] == "") { + while (aOrigins[i + 1] == aOrigins[i]) { + i++; + } + data[aOrigins[i]] = null; + } else { + let hostData = {}; + hostData[aKeys[i]] = aValues[i]; + while (aOrigins[i + 1] == aOrigins[i]) { + i++; + hostData[aKeys[i]] = aValues[i]; + } + data[aOrigins[i]] = hostData; + } + } + if (aOrigins.length) { + return data; + } + + return null; + }, + + updateSessionStore: function SSF_updateSessionStore( + aBrowser, + aBrowsingContext, + aFlushId, + aIsFinal, + aEpoch, + aData, + aCollectSHistory + ) { + let currentData = {}; + if (aData.docShellCaps != undefined) { + currentData.disallow = aData.docShellCaps ? aData.docShellCaps : null; + } + if (aData.isPrivate != undefined) { + currentData.isPrivate = aData.isPrivate; + } + if ( + aData.positions != undefined && + aData.positionDescendants != undefined + ) { + currentData.scroll = this.updateScrollPositions( + aData.positions, + aData.positionDescendants + ); + } + if (aData.id != undefined) { + this.updateFormData("id", aData.id); + } + if (aData.xpath != undefined) { + this.updateFormData("XPath", aData.xpath); + } + if (aData.inputDescendants != undefined) { + this.updateInput( + currentData, + aData.inputDescendants, + aData.innerHTML, + aData.url, + aData.numId, + aData.numXPath + ); + } + if (aData.isFullStorage != undefined) { + let storage = this.updateStorage( + aData.storageOrigins, + aData.storageKeys, + aData.storageValues + ); + if (aData.isFullStorage) { + currentData.storage = storage; + } else { + currentData.storagechange = storage; + } + } + + SessionStore.updateSessionStoreFromTablistener(aBrowser, aBrowsingContext, { + data: currentData, + flushID: aFlushId, + isFinal: aIsFinal, + epoch: aEpoch, + sHistoryNeeded: aCollectSHistory, + }); + this._formDataId = []; + this._formDataIdValue = []; + this._formDataXPath = []; + this._formDataXPathValue = []; + }, +}; diff --git a/toolkit/components/sessionstore/SessionStoreListener.cpp b/toolkit/components/sessionstore/SessionStoreListener.cpp new file mode 100644 index 0000000000..b3dfddf1d8 --- /dev/null +++ b/toolkit/components/sessionstore/SessionStoreListener.cpp @@ -0,0 +1,852 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/PresShell.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/SessionStoreListener.h" +#include "mozilla/dom/SessionStoreUtils.h" +#include "mozilla/dom/SessionStoreUtilsBinding.h" +#include "mozilla/dom/StorageEvent.h" +#include "mozilla/dom/BrowserChild.h" +#include "mozilla/StaticPrefs_browser.h" +#include "nsGenericHTMLElement.h" +#include "nsDocShell.h" +#include "nsIAppWindow.h" +#include "nsIDocShell.h" +#include "nsIDocShellTreeOwner.h" +#include "nsImportModule.h" +#include "nsIPrefBranch.h" +#include "nsIPrefService.h" +#include "nsITimer.h" +#include "nsIWebProgress.h" +#include "nsIXPConnect.h" +#include "nsIXULRuntime.h" +#include "nsPresContext.h" +#include "nsPrintfCString.h" +#include "SessionStoreFunctions.h" + +using namespace mozilla; +using namespace mozilla::dom; + +// This pref controls whether or not we send updates to the parent on a timeout +// or not, and should only be used for tests or debugging. +static const char kTimeOutDisable[] = + "browser.sessionstore.debug.no_auto_updates"; +// Timeout for waiting an idle period to send data. +static const char kPrefInterval[] = "browser.sessionstore.interval"; + +NS_IMPL_CYCLE_COLLECTION(ContentSessionStore, mDocShell) +NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(ContentSessionStore, AddRef) +NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(ContentSessionStore, Release) + +ContentSessionStore::ContentSessionStore(nsIDocShell* aDocShell) + : mDocShell(aDocShell), + mPrivateChanged(false), + mIsPrivate(false), + mScrollChanged(NO_CHANGE), + mFormDataChanged(NO_CHANGE), + mStorageStatus(NO_STORAGE), + mDocCapChanged(false), + mSHistoryChanged(false), + mSHistoryChangedFromParent(false) { + MOZ_ASSERT(mDocShell); + // Check that value at startup as it might have + // been set before the frame script was loaded. + if (NS_SUCCEEDED(nsDocShell::Cast(mDocShell)->GetUsePrivateBrowsing( + &mPrivateChanged)) && + mPrivateChanged) { + mIsPrivate = true; + } +} + +nsCString ContentSessionStore::CollectDocShellCapabilities() { + bool allow; + nsCString aRetVal; + +#define TRY_ALLOWPROP(y) \ + PR_BEGIN_MACRO \ + nsresult rv = mDocShell->GetAllow##y(&allow); \ + if (NS_SUCCEEDED(rv) && !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 + return aRetVal; +} + +void ContentSessionStore::OnPrivateModeChanged(bool aEnabled) { + mPrivateChanged = true; + mIsPrivate = aEnabled; +} + +nsCString ContentSessionStore::GetDocShellCaps() { + mDocCapChanged = false; + return mDocCaps; +} + +bool ContentSessionStore::GetPrivateModeEnabled() { + mPrivateChanged = false; + return mIsPrivate; +} + +void ContentSessionStore::SetFullStorageNeeded() { + // We need the entire session storage, reset the pending individual change + ResetStorageChanges(); + mStorageStatus = FULLSTORAGE; +} + +void ContentSessionStore::ResetStorageChanges() { + mOrigins.Clear(); + mKeys.Clear(); + mValues.Clear(); +} + +void ContentSessionStore::SetSHistoryChanged() { + mSHistoryChanged = mozilla::SessionHistoryInParent(); +} + +// Request "collect sessionHistory" from the parent process +void ContentSessionStore::SetSHistoryFromParentChanged() { + mSHistoryChangedFromParent = mozilla::SessionHistoryInParent(); +} + +void ContentSessionStore::OnDocumentStart() { + mScrollChanged = PAGELOADEDSTART; + mFormDataChanged = PAGELOADEDSTART; + nsCString caps = CollectDocShellCapabilities(); + if (!mDocCaps.Equals(caps)) { + mDocCaps = caps; + mDocCapChanged = true; + } + + SetFullStorageNeeded(); + + if (mozilla::SessionHistoryInParent()) { + mSHistoryChanged = true; + } +} + +void ContentSessionStore::OnDocumentEnd() { + mScrollChanged = WITH_CHANGE; + SetFullStorageNeeded(); + + if (mozilla::SessionHistoryInParent()) { + mSHistoryChanged = true; + } +} + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TabListener) + NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener) + NS_INTERFACE_MAP_ENTRY(nsIObserver) + NS_INTERFACE_MAP_ENTRY(nsIPrivacyTransitionObserver) + NS_INTERFACE_MAP_ENTRY(nsIWebProgressListener) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIDOMEventListener) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION_WEAK(TabListener, mDocShell, mSessionStore, + mOwnerContent) +NS_IMPL_CYCLE_COLLECTING_ADDREF(TabListener) +NS_IMPL_CYCLE_COLLECTING_RELEASE(TabListener) + +TabListener::TabListener(nsIDocShell* aDocShell, Element* aElement) + : mDocShell(aDocShell), + mSessionStore(new ContentSessionStore(aDocShell)), + mOwnerContent(aElement), + mProgressListenerRegistered(false), + mEventListenerRegistered(false), + mPrefObserverRegistered(false), + mStorageObserverRegistered(false), + mStorageChangeListenerRegistered(false), + mUpdatedTimer(nullptr), + mTimeoutDisabled(false), + mUpdateInterval(15000), + mEpoch(0) { + MOZ_ASSERT(mDocShell); +} + +EventTarget* TabListener::GetEventTarget() { + if (mOwnerContent) { + return mOwnerContent; + } + + nsCOMPtr<nsPIDOMWindowOuter> window = do_GetInterface(mDocShell); + if (window) { + return window->GetChromeEventHandler(); + } + + return nullptr; +} + +nsresult TabListener::Init() { + TabListener::UpdateSessionStore(); + nsresult rv = mDocShell->AddWeakPrivacyTransitionObserver(this); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIWebProgress> webProgress = do_QueryInterface(mDocShell); + rv = webProgress->AddProgressListener(this, + nsIWebProgress::NOTIFY_STATE_DOCUMENT); + NS_ENSURE_SUCCESS(rv, rv); + mProgressListenerRegistered = true; + + nsCOMPtr<nsIPrefBranch> prefBranch = do_GetService(NS_PREFSERVICE_CONTRACTID); + NS_WARNING_ASSERTION(prefBranch, "no prefservice"); + if (prefBranch) { + prefBranch->AddObserver(kTimeOutDisable, this, true); + prefBranch->AddObserver(kPrefInterval, this, true); + mPrefObserverRegistered = true; + } + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + NS_WARNING_ASSERTION(obs, "no observer service"); + if (obs) { + obs->AddObserver(this, "browser:purge-sessionStorage", true); + mStorageObserverRegistered = true; + } + + nsCOMPtr<EventTarget> eventTarget = GetEventTarget(); + if (!eventTarget) { + return NS_OK; + } + eventTarget->AddSystemEventListener(u"mozvisualscroll"_ns, this, false); + eventTarget->AddSystemEventListener(u"input"_ns, this, false); + + if (mozilla::SessionHistoryInParent()) { + eventTarget->AddSystemEventListener(u"DOMTitleChanged"_ns, this, false); + } + + mEventListenerRegistered = true; + eventTarget->AddSystemEventListener(u"MozSessionStorageChanged"_ns, this, + false); + mStorageChangeListenerRegistered = true; + return NS_OK; +} + +/* static */ +void TabListener::TimerCallback(nsITimer* aTimer, void* aClosure) { + auto listener = static_cast<TabListener*>(aClosure); + listener->UpdateSessionStore(); + listener->StopTimerForUpdate(); +} + +void TabListener::StopTimerForUpdate() { + if (mUpdatedTimer) { + mUpdatedTimer->Cancel(); + mUpdatedTimer = nullptr; + } +} + +void TabListener::AddTimerForUpdate() { + if (mUpdatedTimer) { + return; + } + + if (mTimeoutDisabled) { + UpdateSessionStore(); + return; + } + + NS_NewTimerWithFuncCallback(getter_AddRefs(mUpdatedTimer), TimerCallback, + this, mUpdateInterval, nsITimer::TYPE_ONE_SHOT, + "TabListener::TimerCallback"); +} + +NS_IMETHODIMP TabListener::PrivateModeChanged(bool aEnabled) { + mSessionStore->OnPrivateModeChanged(aEnabled); + AddTimerForUpdate(); + return NS_OK; +} + +NS_IMETHODIMP TabListener::OnStateChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + uint32_t aStateFlags, + nsresult aStatus) { + if (!mSessionStore) { + return NS_OK; + } + + // Ignore state changes for subframes because we're only interested in the + // top-document starting or stopping its load. + bool isTopLevel = false; + nsresult rv = aWebProgress->GetIsTopLevel(&isTopLevel); + NS_ENSURE_SUCCESS(rv, rv); + if (!isTopLevel) { + return NS_OK; + } + + nsCOMPtr<nsIWebProgress> webProgress = do_QueryInterface(mDocShell); + if (webProgress != aWebProgress) { + return NS_OK; + } + + // onStateChange will be fired when loading the initial about:blank URI for + // a browser, which we don't actually care about. This is particularly for + // the case of unrestored background tabs, where the content has not yet + // been restored: we don't want to accidentally send any updates to the + // parent when the about:blank placeholder page has loaded. + if (!mDocShell->GetHasLoadedNonBlankURI()) { + return NS_OK; + } + + if (aStateFlags & (nsIWebProgressListener::STATE_START)) { + mSessionStore->OnDocumentStart(); + ResetStorageChangeListener(); + } else if (aStateFlags & (nsIWebProgressListener::STATE_STOP)) { + mSessionStore->OnDocumentEnd(); + } + + return NS_OK; +} + +NS_IMETHODIMP +TabListener::HandleEvent(Event* aEvent) { + EventTarget* target = aEvent->GetTarget(); + if (!target) { + return NS_OK; + } + + nsPIDOMWindowOuter* outer = target->GetOwnerGlobalForBindingsInternal(); + if (!outer || !outer->GetDocShell()) { + return NS_OK; + } + + RefPtr<BrowsingContext> context = outer->GetBrowsingContext(); + if (!context || context->CreatedDynamically()) { + return NS_OK; + } + + nsAutoString eventType; + aEvent->GetType(eventType); + if (eventType.EqualsLiteral("mozvisualscroll")) { + mSessionStore->SetScrollPositionChanged(); + AddTimerForUpdate(); + } else if (eventType.EqualsLiteral("input")) { + mSessionStore->SetFormDataChanged(); + AddTimerForUpdate(); + } else if (eventType.EqualsLiteral("MozSessionStorageChanged")) { + auto event = static_cast<StorageEvent*>(aEvent); + RefPtr<Storage> changingStorage = event->GetStorageArea(); + if (!changingStorage) { + return NS_OK; + } + // How much data does DOMSessionStorage contain? + int64_t storageUsage = changingStorage->GetOriginQuotaUsage(); + if (storageUsage > StaticPrefs::browser_sessionstore_dom_storage_limit()) { + RemoveStorageChangeListener(); + mSessionStore->ResetStorageChanges(); + mSessionStore->ResetStorage(); + return NS_OK; + } + if (mSessionStore->AppendSessionStorageChange(event)) { + AddTimerForUpdate(); + } + } else if (eventType.EqualsLiteral("DOMTitleChanged")) { + mSessionStore->SetSHistoryChanged(); + AddTimerForUpdate(); + } + + return NS_OK; +} + +NS_IMETHODIMP TabListener::OnProgressChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + int32_t aCurSelfProgress, + int32_t aMaxSelfProgress, + int32_t aCurTotalProgress, + int32_t aMaxTotalProgress) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP TabListener::OnLocationChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + nsIURI* aLocation, + uint32_t aFlags) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP TabListener::OnStatusChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + nsresult aStatus, + const char16_t* aMessage) { + return NS_ERROR_NOT_IMPLEMENTED; +} +NS_IMETHODIMP TabListener::OnSecurityChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + uint32_t aState) { + return NS_ERROR_NOT_IMPLEMENTED; +} +NS_IMETHODIMP TabListener::OnContentBlockingEvent(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + uint32_t aEvent) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +nsresult TabListener::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + if (!strcmp(aTopic, "browser:purge-sessionStorage")) { + mSessionStore->SetFullStorageNeeded(); + AddTimerForUpdate(); + return NS_OK; + } + + if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) { + nsCOMPtr<nsIPrefBranch> prefBranch = do_QueryInterface(aSubject); + + bool timeoutDisabled; + if (NS_SUCCEEDED( + prefBranch->GetBoolPref(kTimeOutDisable, &timeoutDisabled))) { + if (mTimeoutDisabled != timeoutDisabled) { + mTimeoutDisabled = timeoutDisabled; + if (mUpdatedTimer) { + StopTimerForUpdate(); + AddTimerForUpdate(); + } + } + } + + int32_t interval = 0; + if (NS_SUCCEEDED(prefBranch->GetIntPref(kPrefInterval, &interval))) { + if (mUpdateInterval != interval) { + mUpdateInterval = interval; + if (mUpdatedTimer) { + StopTimerForUpdate(); + AddTimerForUpdate(); + } + } + } + return NS_OK; + } + + NS_ERROR("Unexpected topic"); + return NS_ERROR_UNEXPECTED; +} + +nsCString CollectPosition(Document& aDocument) { + PresShell* presShell = aDocument.GetPresShell(); + if (!presShell) { + return ""_ns; + } + nsPoint scrollPos = presShell->GetVisualViewportOffset(); + int scrollX = nsPresContext::AppUnitsToIntCSSPixels(scrollPos.x); + int scrollY = nsPresContext::AppUnitsToIntCSSPixels(scrollPos.y); + if ((scrollX != 0) || (scrollY != 0)) { + return nsPrintfCString("%d,%d", scrollX, scrollY); + } + + return ""_ns; +} + +int CollectPositions(BrowsingContext* aBrowsingContext, + nsTArray<nsCString>& aPositions, + nsTArray<int32_t>& aPositionDescendants) { + if (aBrowsingContext->CreatedDynamically()) { + return 0; + } + + nsPIDOMWindowOuter* window = aBrowsingContext->GetDOMWindow(); + if (!window) { + return 0; + } + + Document* document = window->GetDoc(); + if (!document) { + return 0; + } + + /* Collect data from current frame */ + aPositions.AppendElement(CollectPosition(*document)); + aPositionDescendants.AppendElement(0); + unsigned long currentIdx = aPositions.Length() - 1; + + /* Collect data from all child frame */ + // This is not going to work for fission. Bug 1572084 for tracking it. + for (auto& child : aBrowsingContext->Children()) { + aPositionDescendants[currentIdx] += + CollectPositions(child, aPositions, aPositionDescendants); + } + + return aPositionDescendants[currentIdx] + 1; +} + +void ContentSessionStore::GetScrollPositions( + nsTArray<nsCString>& aPositions, nsTArray<int32_t>& aPositionDescendants) { + if (mScrollChanged == PAGELOADEDSTART) { + aPositionDescendants.AppendElement(0); + aPositions.AppendElement(""_ns); + } else { + CollectPositions(mDocShell->GetBrowsingContext(), aPositions, + aPositionDescendants); + } + mScrollChanged = NO_CHANGE; +} + +void CollectInput(Document& aDocument, InputFormData& aInput, + nsTArray<CollectedInputDataValue>& aIdVals, + nsTArray<CollectedInputDataValue>& aXPathVals) { + PresShell* presShell = aDocument.GetPresShell(); + if (!presShell) { + return; + } + + uint16_t numXPath = 0; + uint16_t numId = 0; + + // textarea element + SessionStoreUtils::CollectFromTextAreaElement(aDocument, numXPath, numId, + aXPathVals, aIdVals); + // input element + SessionStoreUtils::CollectFromInputElement(aDocument, numXPath, numId, + aXPathVals, aIdVals); + // select element + SessionStoreUtils::CollectFromSelectElement(aDocument, numXPath, numId, + aXPathVals, aIdVals); + + Element* bodyElement = aDocument.GetBody(); + if (aDocument.HasFlag(NODE_IS_EDITABLE) && bodyElement) { + bodyElement->GetInnerHTML(aInput.innerHTML, IgnoreErrors()); + } + if (aInput.innerHTML.IsEmpty() && numXPath == 0 && numId == 0) { + 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(aInput.url); + } + aInput.numId = numId; + aInput.numXPath = numXPath; +} + +int CollectInputs(BrowsingContext* aBrowsingContext, + nsTArray<InputFormData>& aInputs, + nsTArray<CollectedInputDataValue>& aIdVals, + nsTArray<CollectedInputDataValue>& aXPathVals) { + if (aBrowsingContext->CreatedDynamically()) { + return 0; + } + + nsPIDOMWindowOuter* window = aBrowsingContext->GetDOMWindow(); + if (!window || !window->GetDocShell()) { + return 0; + } + + Document* document = window->GetDoc(); + if (!document) { + return 0; + } + + /* Collect data from current frame */ + InputFormData input; + input.descendants = 0; + input.numId = 0; + input.numXPath = 0; + CollectInput(*document, input, aIdVals, aXPathVals); + aInputs.AppendElement(input); + unsigned long currentIdx = aInputs.Length() - 1; + + /* Collect data from all child frame */ + // This is not going to work for fission. Bug 1572084 for tracking it. + for (auto& child : aBrowsingContext->Children()) { + aInputs[currentIdx].descendants += + CollectInputs(child, aInputs, aIdVals, aXPathVals); + } + + return aInputs[currentIdx].descendants + 1; +} + +nsTArray<InputFormData> ContentSessionStore::GetInputs( + nsTArray<CollectedInputDataValue>& aIdVals, + nsTArray<CollectedInputDataValue>& aXPathVals) { + nsTArray<InputFormData> inputs; + if (mFormDataChanged == PAGELOADEDSTART) { + mFormDataChanged = NO_CHANGE; + InputFormData input; + input.descendants = 0; + input.innerHTML.Truncate(); + input.url.Truncate(); + input.numId = 0; + input.numXPath = 0; + inputs.AppendElement(input); + } else { + mFormDataChanged = NO_CHANGE; + CollectInputs(nsDocShell::Cast(mDocShell)->GetBrowsingContext(), inputs, + aIdVals, aXPathVals); + } + return inputs; +} + +bool ContentSessionStore::AppendSessionStorageChange(StorageEvent* aEvent) { + // We will collect the full SessionStore if mStorageStatus is FULLSTORAGE. + // These partial changes can be skipped in this case. + if (mStorageStatus == FULLSTORAGE) { + return false; + } + + RefPtr<Storage> changingStorage = aEvent->GetStorageArea(); + if (!changingStorage) { + return false; + } + + nsCOMPtr<nsIPrincipal> storagePrincipal = changingStorage->StoragePrincipal(); + if (!storagePrincipal) { + return false; + } + + nsAutoCString origin; + nsresult rv = storagePrincipal->GetOrigin(origin); + if (NS_FAILED(rv)) { + return false; + } + + mOrigins.AppendElement(origin); + aEvent->GetKey(*mKeys.AppendElement()); + aEvent->GetNewValue(*mValues.AppendElement()); + mStorageStatus = STORAGECHANGE; + return true; +} + +bool ContentSessionStore::GetAndClearStorageChanges( + nsTArray<nsCString>& aOrigins, nsTArray<nsString>& aKeys, + nsTArray<nsString>& aValues) { + MOZ_ASSERT(IsStorageUpdated()); + bool isFullStorage = false; + + if (mStorageStatus == RESET) { + isFullStorage = true; + } else if (mStorageStatus == FULLSTORAGE) { + MOZ_ASSERT(mDocShell); + SessionStoreUtils::CollectedSessionStorage( + nsDocShell::Cast(mDocShell)->GetBrowsingContext(), aOrigins, aKeys, + aValues); + isFullStorage = true; + } else if (mStorageStatus == STORAGECHANGE) { + aOrigins.SwapElements(mOrigins); + aKeys.SwapElements(mKeys); + aValues.SwapElements(mValues); + } + + ResetStorageChanges(); + mStorageStatus = NO_STORAGE; + return isFullStorage; +} + +bool TabListener::ForceFlushFromParent(uint32_t aFlushId, bool aIsFinal) { + if (!XRE_IsParentProcess()) { + return false; + } + if (!mSessionStore) { + return false; + } + return UpdateSessionStore(aFlushId, aIsFinal); +} + +void TabListener::UpdateSHistoryChanges(bool aImmediately) { + mSessionStore->SetSHistoryFromParentChanged(); + if (aImmediately) { + UpdateSessionStore(); + } else { + AddTimerForUpdate(); + } +} + +bool TabListener::UpdateSessionStore(uint32_t aFlushId, bool aIsFinal) { + if (!aFlushId) { + if (!mSessionStore || !mSessionStore->UpdateNeeded()) { + return false; + } + } + + if (!XRE_IsParentProcess()) { + BrowserChild* browserChild = BrowserChild::GetFrom(mDocShell); + if (browserChild) { + StopTimerForUpdate(); + return browserChild->UpdateSessionStore(aFlushId); + } + return false; + } + + if (!mOwnerContent) { + return false; + } + + uint32_t chromeFlags = 0; + nsCOMPtr<nsIDocShellTreeOwner> treeOwner; + mDocShell->GetTreeOwner(getter_AddRefs(treeOwner)); + if (!treeOwner) { + return false; + } + nsCOMPtr<nsIAppWindow> window(do_GetInterface(treeOwner)); + if (!window) { + return false; + } + if (window && NS_FAILED(window->GetChromeFlags(&chromeFlags))) { + return false; + } + + UpdateSessionStoreData data; + if (mSessionStore->IsDocCapChanged()) { + data.mDocShellCaps.Construct() = mSessionStore->GetDocShellCaps(); + } + if (mSessionStore->IsPrivateChanged()) { + data.mIsPrivate.Construct() = mSessionStore->GetPrivateModeEnabled(); + } + if (mSessionStore->IsScrollPositionChanged()) { + nsTArray<nsCString> positions; + nsTArray<int> descendants; + mSessionStore->GetScrollPositions(positions, descendants); + data.mPositions.Construct(std::move(positions)); + data.mPositionDescendants.Construct(std::move(descendants)); + } + if (mSessionStore->IsFormDataChanged()) { + nsTArray<CollectedInputDataValue> dataWithId, dataWithXpath; + nsTArray<InputFormData> inputs = + mSessionStore->GetInputs(dataWithId, dataWithXpath); + nsTArray<int> descendants, numId, numXPath; + nsTArray<nsString> innerHTML; + nsTArray<nsCString> url; + + if (dataWithId.Length() != 0) { + SessionStoreUtils::ComposeInputData(dataWithId, data.mId.Construct()); + } + if (dataWithXpath.Length() != 0) { + SessionStoreUtils::ComposeInputData(dataWithXpath, + data.mXpath.Construct()); + } + + for (const InputFormData& input : inputs) { + descendants.AppendElement(input.descendants); + numId.AppendElement(input.numId); + numXPath.AppendElement(input.numXPath); + innerHTML.AppendElement(input.innerHTML); + url.AppendElement(input.url); + } + if (descendants.Length() != 0) { + data.mInputDescendants.Construct(std::move(descendants)); + data.mNumId.Construct(std::move(numId)); + data.mNumXPath.Construct(std::move(numXPath)); + data.mInnerHTML.Construct(std::move(innerHTML)); + data.mUrl.Construct(std::move(url)); + } + } + if (mSessionStore->IsStorageUpdated()) { + nsTArray<nsCString> origins; + nsTArray<nsString> keys, values; + data.mIsFullStorage.Construct() = + mSessionStore->GetAndClearStorageChanges(origins, keys, values); + data.mStorageOrigins.Construct(std::move(origins)); + data.mStorageKeys.Construct(std::move(keys)); + data.mStorageValues.Construct(std::move(values)); + } + + nsCOMPtr<nsISessionStoreFunctions> funcs = + do_ImportModule("resource://gre/modules/SessionStoreFunctions.jsm"); + NS_ENSURE_TRUE(funcs, false); + nsCOMPtr<nsIXPConnectWrappedJS> wrapped = do_QueryInterface(funcs); + AutoJSAPI jsapi; + MOZ_ALWAYS_TRUE(jsapi.Init(wrapped->GetJSObjectGlobal())); + JS::Rooted<JS::Value> dataVal(jsapi.cx()); + bool ok = ToJSValue(jsapi.cx(), data, &dataVal); + NS_ENSURE_TRUE(ok, false); + + nsresult rv = funcs->UpdateSessionStore( + mOwnerContent, mDocShell->GetBrowsingContext(), aFlushId, aIsFinal, + mEpoch, dataVal, mSessionStore->GetAndClearSHistoryChanged()); + NS_ENSURE_SUCCESS(rv, false); + StopTimerForUpdate(); + return true; +} + +void TabListener::ResetStorageChangeListener() { + if (mStorageChangeListenerRegistered) { + return; + } + + nsCOMPtr<EventTarget> eventTarget = GetEventTarget(); + if (!eventTarget) { + return; + } + eventTarget->AddSystemEventListener(u"MozSessionStorageChanged"_ns, this, + false); + mStorageChangeListenerRegistered = true; +} + +void TabListener::RemoveStorageChangeListener() { + if (!mStorageChangeListenerRegistered) { + return; + } + + nsCOMPtr<EventTarget> eventTarget = GetEventTarget(); + if (eventTarget) { + eventTarget->RemoveSystemEventListener(u"MozSessionStorageChanged"_ns, this, + false); + mStorageChangeListenerRegistered = false; + } +} + +void TabListener::RemoveListeners() { + if (mProgressListenerRegistered) { + nsCOMPtr<nsIWebProgress> webProgress = do_QueryInterface(mDocShell); + if (webProgress) { + webProgress->RemoveProgressListener(this); + mProgressListenerRegistered = false; + } + } + + if (mEventListenerRegistered || mStorageChangeListenerRegistered) { + nsCOMPtr<EventTarget> eventTarget = GetEventTarget(); + if (eventTarget) { + if (mEventListenerRegistered) { + eventTarget->RemoveSystemEventListener(u"mozvisualscroll"_ns, this, + false); + eventTarget->RemoveSystemEventListener(u"input"_ns, this, false); + if (mozilla::SessionHistoryInParent()) { + eventTarget->RemoveSystemEventListener(u"DOMTitleChanged"_ns, this, + false); + } + mEventListenerRegistered = false; + } + if (mStorageChangeListenerRegistered) { + eventTarget->RemoveSystemEventListener(u"MozSessionStorageChanged"_ns, + this, false); + mStorageChangeListenerRegistered = false; + } + } + } + + if (mPrefObserverRegistered || mStorageObserverRegistered) { + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (!obs) { + return; + } + if (mPrefObserverRegistered) { + obs->RemoveObserver(this, kTimeOutDisable); + obs->RemoveObserver(this, kPrefInterval); + mPrefObserverRegistered = false; + } + if (mStorageObserverRegistered) { + obs->RemoveObserver(this, "browser:purge-sessionStorage"); + mStorageObserverRegistered = false; + } + } +} + +TabListener::~TabListener() { RemoveListeners(); } diff --git a/toolkit/components/sessionstore/SessionStoreListener.h b/toolkit/components/sessionstore/SessionStoreListener.h new file mode 100644 index 0000000000..e2f79cb5e6 --- /dev/null +++ b/toolkit/components/sessionstore/SessionStoreListener.h @@ -0,0 +1,174 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/. */ + +#ifndef mozilla_dom_SessionStoreListener_h +#define mozilla_dom_SessionStoreListener_h + +#include "nsIDOMEventListener.h" +#include "nsIPrivacyTransitionObserver.h" +#include "nsIWebProgressListener.h" +#include "SessionStoreData.h" + +class nsITimer; + +namespace mozilla { +namespace dom { + +class StorageEvent; + +class ContentSessionStore { + public: + explicit ContentSessionStore(nsIDocShell* aDocShell); + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(ContentSessionStore) + NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(ContentSessionStore) + + void OnPrivateModeChanged(bool aEnabled); + bool IsDocCapChanged() { return mDocCapChanged; } + nsCString GetDocShellCaps(); + bool IsPrivateChanged() { return mPrivateChanged; } + bool GetPrivateModeEnabled(); + void SetScrollPositionChanged() { mScrollChanged = WITH_CHANGE; } + bool IsScrollPositionChanged() { return mScrollChanged != NO_CHANGE; } + void GetScrollPositions(nsTArray<nsCString>& aPositions, + nsTArray<int32_t>& aPositionDescendants); + void SetFormDataChanged() { mFormDataChanged = WITH_CHANGE; } + bool IsFormDataChanged() { return mFormDataChanged != NO_CHANGE; } + nsTArray<InputFormData> GetInputs( + nsTArray<CollectedInputDataValue>& aIdVals, + nsTArray<CollectedInputDataValue>& aXPathVals); + + // Use "mStorageStatus" to manage the status of storageChanges + bool IsStorageUpdated() { return mStorageStatus != NO_STORAGE; } + void ResetStorage() { mStorageStatus = RESET; } + /* + There are three situations we need entire session storage: + 1. OnDocumentStart: PageLoad started + 2. OnDocumentEnd: PageLoad completed + 3. receive "browser:purge-sessionStorage" event + Use SetFullStorageNeeded() to set correct "mStorageStatus" and + reset the pending individual change. + */ + void SetFullStorageNeeded(); + void ResetStorageChanges(); + // GetAndClearStorageChanges() is used for getting storageChanges. + // It clears the stored storage changes before returning. + // It will return true if it is a entire session storage. + // Otherwise, it will return false. + bool GetAndClearStorageChanges(nsTArray<nsCString>& aOrigins, + nsTArray<nsString>& aKeys, + nsTArray<nsString>& aValues); + // Using AppendSessionStorageChange() to append session storage change when + // receiving "MozSessionStorageChanged". + // Return true if there is a new storage change which is appended. + bool AppendSessionStorageChange(StorageEvent* aEvent); + + void SetSHistoryChanged(); + // request "collect sessionHistory" which is happened in the parent process + void SetSHistoryFromParentChanged(); + bool GetAndClearSHistoryChanged() { + bool ret = mSHistoryChanged; + mSHistoryChanged = false; + mSHistoryChangedFromParent = false; + return ret; + } + + void OnDocumentStart(); + void OnDocumentEnd(); + bool UpdateNeeded() { + return mPrivateChanged || mDocCapChanged || IsScrollPositionChanged() || + IsFormDataChanged() || IsStorageUpdated() || mSHistoryChanged || + mSHistoryChangedFromParent; + } + + private: + virtual ~ContentSessionStore() = default; + nsCString CollectDocShellCapabilities(); + + nsCOMPtr<nsIDocShell> mDocShell; + bool mPrivateChanged; + bool mIsPrivate; + enum { + NO_CHANGE, + PAGELOADEDSTART, // set when the state of document is STATE_START + WITH_CHANGE, // set when the change event is observed + } mScrollChanged, + mFormDataChanged; + enum { + NO_STORAGE, + RESET, + FULLSTORAGE, + STORAGECHANGE, + } mStorageStatus; + bool mDocCapChanged; + nsCString mDocCaps; + // mOrigins, mKeys, mValues are for sessionStorage partial changes + nsTArray<nsCString> mOrigins; + nsTArray<nsString> mKeys; + nsTArray<nsString> mValues; + // mSHistoryChanged means there are history changes which are found + // in the child process. The flag is set when + // 1. webProgress changes to STATE_START + // 2. webProgress changes to STATE_STOP + // 3. receiving "DOMTitleChanged" event + bool mSHistoryChanged; + // mSHistoryChangedFromParent means there are history changes which + // are found by session history listener in the parent process. + bool mSHistoryChangedFromParent; +}; + +class TabListener : public nsIDOMEventListener, + public nsIObserver, + public nsIPrivacyTransitionObserver, + public nsIWebProgressListener, + public nsSupportsWeakReference { + public: + explicit TabListener(nsIDocShell* aDocShell, Element* aElement); + EventTarget* GetEventTarget(); + nsresult Init(); + ContentSessionStore* GetSessionStore() { return mSessionStore; } + // the function is called only when TabListener is in parent process + bool ForceFlushFromParent(uint32_t aFlushId, bool aIsFinal = false); + void RemoveListeners(); + void SetEpoch(uint32_t aEpoch) { mEpoch = aEpoch; } + uint32_t GetEpoch() { return mEpoch; } + void UpdateSHistoryChanges(bool aImmediately); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(TabListener, nsIDOMEventListener) + + NS_DECL_NSIDOMEVENTLISTENER + NS_DECL_NSIOBSERVER + NS_DECL_NSIPRIVACYTRANSITIONOBSERVER + NS_DECL_NSIWEBPROGRESSLISTENER + + private: + static void TimerCallback(nsITimer* aTimer, void* aClosure); + void AddTimerForUpdate(); + void StopTimerForUpdate(); + bool UpdateSessionStore(uint32_t aFlushId = 0, bool aIsFinal = false); + void ResetStorageChangeListener(); + void RemoveStorageChangeListener(); + virtual ~TabListener(); + + nsCOMPtr<nsIDocShell> mDocShell; + RefPtr<ContentSessionStore> mSessionStore; + RefPtr<mozilla::dom::Element> mOwnerContent; + bool mProgressListenerRegistered; + bool mEventListenerRegistered; + bool mPrefObserverRegistered; + bool mStorageObserverRegistered; + bool mStorageChangeListenerRegistered; + // Timer used to update data + nsCOMPtr<nsITimer> mUpdatedTimer; + bool mTimeoutDisabled; + int32_t mUpdateInterval; + uint32_t mEpoch; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_SessionStoreListener_h diff --git a/toolkit/components/sessionstore/SessionStoreMessageUtils.h b/toolkit/components/sessionstore/SessionStoreMessageUtils.h new file mode 100644 index 0000000000..26f82273ca --- /dev/null +++ b/toolkit/components/sessionstore/SessionStoreMessageUtils.h @@ -0,0 +1,71 @@ +/* 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/. */ + +#ifndef mozilla_dom_SessionStoreMessageUtils_h +#define mozilla_dom_SessionStoreMessageUtils_h + +#include "ipc/IPCMessageUtils.h" +#include "SessionStoreData.h" + +namespace IPC { + +template <> +struct ParamTraits<mozilla::dom::CollectedNonMultipleSelectValue> { + typedef mozilla::dom::CollectedNonMultipleSelectValue paramType; + + static void Write(Message* aMsg, const paramType& aParam) { + WriteParam(aMsg, aParam.mSelectedIndex); + WriteParam(aMsg, aParam.mValue); + } + + static bool Read(const Message* aMsg, PickleIterator* aIter, + paramType* aResult) { + return ReadParam(aMsg, aIter, &aResult->mSelectedIndex) && + ReadParam(aMsg, aIter, &aResult->mValue); + } +}; + +template <> +struct ParamTraits<CollectedInputDataValue> { + typedef CollectedInputDataValue paramType; + + static void Write(Message* aMsg, const paramType& aParam) { + WriteParam(aMsg, aParam.id); + WriteParam(aMsg, aParam.type); + WriteParam(aMsg, aParam.value); + } + + static bool Read(const Message* aMsg, PickleIterator* aIter, + paramType* aResult) { + return ReadParam(aMsg, aIter, &aResult->id) && + ReadParam(aMsg, aIter, &aResult->type) && + ReadParam(aMsg, aIter, &aResult->value); + } +}; + +template <> +struct ParamTraits<InputFormData> { + typedef InputFormData paramType; + + static void Write(Message* aMsg, const paramType& aParam) { + WriteParam(aMsg, aParam.descendants); + WriteParam(aMsg, aParam.innerHTML); + WriteParam(aMsg, aParam.url); + WriteParam(aMsg, aParam.numId); + WriteParam(aMsg, aParam.numXPath); + } + + static bool Read(const Message* aMsg, PickleIterator* aIter, + paramType* aResult) { + return ReadParam(aMsg, aIter, &aResult->descendants) && + ReadParam(aMsg, aIter, &aResult->innerHTML) && + ReadParam(aMsg, aIter, &aResult->url) && + ReadParam(aMsg, aIter, &aResult->numId) && + ReadParam(aMsg, aIter, &aResult->numXPath); + } +}; + +} // namespace IPC + +#endif // mozilla_dom_SessionStoreMessageUtils_h diff --git a/toolkit/components/sessionstore/SessionStoreUtils.cpp b/toolkit/components/sessionstore/SessionStoreUtils.cpp new file mode 100644 index 0000000000..4bdc191ccb --- /dev/null +++ b/toolkit/components/sessionstore/SessionStoreUtils.cpp @@ -0,0 +1,1366 @@ +/* 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<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()) { + continue; + } + + nsCOMPtr<nsIDocShell> 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<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( + 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<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()) { + 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<Document> 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<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; +} + +// 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<CollectedInputDataValue>& aXPathVals, + nsTArray<CollectedInputDataValue>& 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<CollectedData>& aRetVal) { + Record<nsString, OwningStringOrBooleanOrObject>::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<CollectedInputDataValue>& aXPathVals, + nsTArray<CollectedInputDataValue>& 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<CollectedData>& aRetVal) { + Record<nsString, OwningStringOrBooleanOrObject>::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<CollectedInputDataValue>& aXPathVals, + nsTArray<CollectedInputDataValue>& 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<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(); +} + +/* for single select value */ +static void AppendValueToCollectedData( + nsINode* aNode, const nsAString& aId, + const CollectedNonMultipleSelectValue& aValue, uint16_t& aNumXPath, + uint16_t& aNumId, nsTArray<CollectedInputDataValue>& aXPathVals, + nsTArray<CollectedInputDataValue>& 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<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); +} + +static void AppendValueToCollectedData( + Document& aDocument, nsINode* aNode, const nsAString& aId, + const nsString& aValue, uint16_t& aNumXPath, uint16_t& aNumId, + nsTArray<CollectedInputDataValue>& aXPathVals, + nsTArray<CollectedInputDataValue>& aIdVals) { + CollectedInputDataValue entry; + entry.type = u"string"_ns; + entry.value = AsVariant(aValue); + AppendEntryToCollectedData(aNode, aId, entry, aNumXPath, aNumId, aXPathVals, + aIdVals); +} + +/* 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 nsTArray<nsString>: file and multipleSelect */ +static void AppendValueToCollectedData( + nsINode* aNode, const nsAString& aId, const nsAString& aValueType, + const nsTArray<nsString>& aValue, uint16_t& aNumXPath, uint16_t& aNumId, + nsTArray<CollectedInputDataValue>& aXPathVals, + nsTArray<CollectedInputDataValue>& aIdVals) { + CollectedInputDataValue entry; + entry.type = aValueType; + entry.value = AsVariant(CopyableTArray(aValue.Clone())); + AppendEntryToCollectedData(aNode, aId, entry, aNumXPath, aNumId, aXPathVals, + aIdVals); +} + +/* 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) { + 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<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() == 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<ArgsT>(args)...); + } else if (input->ControlType() == NS_FORM_INPUT_FILE) { + 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 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); + + Element* bodyElement = aDocument.GetBody(); + if (aDocument.HasFlag(NODE_IS_EDITABLE) && bodyElement) { + 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; + HTMLTextAreaElement* textArea = HTMLTextAreaElement::FromNode(aElement); + if (textArea) { + textArea->SetValue(aValue, rv); + if (!rv.Failed()) { + nsContentUtils::DispatchInputEvent(aElement); + } + return; + } + HTMLInputElement* input = HTMLInputElement::FromNode(aElement); + if (input) { + input->SetValue(aValue, CallerType::NonSystem, rv); + if (!rv.Failed()) { + nsContentUtils::DispatchInputEvent(aElement); + return; + } + } + input = HTMLInputElement::FromNodeOrNull( + nsFocusManager::GetRedirectedFocus(aElement)); + if (input) { + input->SetValue(aValue, CallerType::NonSystem, rv); + if (!rv.Failed()) { + nsContentUtils::DispatchInputEvent(aElement); + } + } +} + +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) { + nsTArray<nsString> fileList; + 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() == NS_FORM_INPUT_FILE) { + 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); + } +} + +MOZ_CAN_RUN_SCRIPT +static void SetRestoreData(JSContext* aCx, Element* aElement, + JS::MutableHandle<JS::Value> aObject) { + nsAutoString data; + if (nsContentUtils::StringifyJSON(aCx, aObject, data)) { + SetElementAsString(aElement, data); + } else { + JS_ClearPendingException(aCx); + } +} + +MOZ_CAN_RUN_SCRIPT +static void SetInnerHTML(Document& aDocument, const CollectedData& aData) { + RefPtr<Element> bodyElement = aDocument.GetBody(); + if (aDocument.HasFlag(NODE_IS_EDITABLE) && bodyElement) { + IgnoredErrorResult rv; + bodyElement->SetInnerHTML(aData.mInnerHTML.Value(), + 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(JSContext* aCx, 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( + aCx, aDocument, XPathResult::FIRST_ORDERED_NODE_TYPE, nullptr, rv); + if (rv.Failed()) { + return nullptr; + } + return Element::FromNodeOrNull(result->GetSingleNodeValue(rv)); +} + +MOZ_CAN_RUN_SCRIPT_BOUNDARY +/* 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; + } + if (aData.mInnerHTML.WasPassed()) { + SetInnerHTML(aDocument, aData); + } + 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")) { + nsAutoCString url; + Unused << aDocument.GetDocumentURI()->GetSpecIgnoringRef(url); + if (url.EqualsLiteral("about:sessionrestore") || + url.EqualsLiteral("about:welcomeback")) { + JS::Rooted<JS::Value> object( + cx, JS::ObjectValue(*entry.mValue.GetAsObject())); + SetRestoreData(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(aGlobal.Context(), 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; +} + +/* Read entries in the session storage data contained in a tab's history. */ +static void ReadAllEntriesFromStorage(nsPIDOMWindowOuter* aWindow, + nsTArray<nsCString>& aOrigins, + nsTArray<nsString>& aKeys, + nsTArray<nsString>& aValues) { + BrowsingContext* const browsingContext = aWindow->GetBrowsingContext(); + if (!browsingContext) { + return; + } + + Document* doc = aWindow->GetDoc(); + if (!doc) { + return; + } + + nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal(); + if (!principal) { + return; + } + + nsCOMPtr<nsIPrincipal> storagePrincipal = doc->EffectiveStoragePrincipal(); + if (!storagePrincipal) { + return; + } + + nsAutoCString origin; + nsresult rv = storagePrincipal->GetOrigin(origin); + if (NS_FAILED(rv) || aOrigins.Contains(origin)) { + // Don't read a host twice. + return; + } + + /* Completed checking for recursion and is about to read storage*/ + const RefPtr<SessionStorageManager> storageManager = + browsingContext->GetSessionStorageManager(); + if (!storageManager) { + return; + } + RefPtr<Storage> storage; + storageManager->GetStorage(aWindow->GetCurrentInnerWindow(), principal, + storagePrincipal, false, getter_AddRefs(storage)); + if (!storage) { + return; + } + mozilla::IgnoredErrorResult result; + uint32_t len = storage->GetLength(*principal, result); + if (result.Failed() || len == 0) { + return; + } + int64_t storageUsage = storage->GetOriginQuotaUsage(); + if (storageUsage > StaticPrefs::browser_sessionstore_dom_storage_limit()) { + return; + } + + for (uint32_t i = 0; i < len; i++) { + nsString key, value; + mozilla::IgnoredErrorResult res; + storage->Key(i, key, *principal, res); + if (res.Failed()) { + continue; + } + + storage->GetItem(key, value, *principal, res); + if (res.Failed()) { + continue; + } + + aKeys.AppendElement(key); + aValues.AppendElement(value); + aOrigins.AppendElement(origin); + } +} + +/* Collect Collect session storage from current frame and all child frame */ +/* static */ +void SessionStoreUtils::CollectedSessionStorage( + BrowsingContext* aBrowsingContext, nsTArray<nsCString>& aOrigins, + nsTArray<nsString>& aKeys, nsTArray<nsString>& aValues) { + /* Collect session store from current frame */ + nsPIDOMWindowOuter* window = aBrowsingContext->GetDOMWindow(); + if (!window) { + return; + } + ReadAllEntriesFromStorage(window, aOrigins, aKeys, aValues); + + /* Collect session storage from all child frame */ + if (!window->GetDocShell()) { + return; + } + + // This is not going to work for fission. Bug 1572084 for tracking it. + for (BrowsingContext* child : aBrowsingContext->Children()) { + if (!child->CreatedDynamically()) { + SessionStoreUtils::CollectedSessionStorage(child, aOrigins, aKeys, + aValues); + } + } +} + +/* static */ +void SessionStoreUtils::RestoreSessionStorage( + const GlobalObject& aGlobal, nsIDocShell* aDocShell, + const Record<nsString, Record<nsString, nsString>>& aData) { + for (auto& entry : aData.Entries()) { + // NOTE: In capture() we record the full origin for the URI which the + // sessionStorage is being captured for. As of bug 1235657 this code + // stopped parsing any origins which have originattributes correctly, as + // it decided to use the origin attributes from the docshell, and try to + // interpret the origin as a URI. Since bug 1353844 this code now correctly + // parses the full origin, and then discards the origin attributes, to + // make the behavior line up with the original intentions in bug 1235657 + // while preserving the ability to read all session storage from + // previous versions. In the future, if this behavior is desired, we may + // want to use the spec instead of the origin as the key, and avoid + // transmitting origin attribute information which we then discard when + // restoring. + // + // If changing this logic, make sure to also change the principal + // computation logic in SessionStore::_sendRestoreHistory. + + // OriginAttributes are always after a '^' character + int32_t pos = entry.mKey.RFindChar('^'); + nsCOMPtr<nsIPrincipal> principal = BasePrincipal::CreateContentPrincipal( + NS_ConvertUTF16toUTF8(Substring(entry.mKey, 0, pos))); + BrowsingContext* const browsingContext = + nsDocShell::Cast(aDocShell)->GetBrowsingContext(); + if (!browsingContext) { + return; + } + + nsCOMPtr<nsIPrincipal> storagePrincipal = + BasePrincipal::CreateContentPrincipal( + NS_ConvertUTF16toUTF8(entry.mKey)); + + const RefPtr<SessionStorageManager> storageManager = + browsingContext->GetSessionStorageManager(); + if (!storageManager) { + return; + } + RefPtr<Storage> storage; + // There is no need to pass documentURI, it's only used to fill documentURI + // property of domstorage event, which in this case has no consumer. + // Prevention of events in case of missing documentURI will be solved in a + // followup bug to bug 600307. + // Null window because the current window doesn't match the principal yet + // and loads about:blank. + storageManager->CreateStorage(nullptr, principal, storagePrincipal, u""_ns, + false, getter_AddRefs(storage)); + if (!storage) { + continue; + } + for (auto& InnerEntry : entry.mValue.Entries()) { + IgnoredErrorResult result; + storage->SetItem(InnerEntry.mKey, InnerEntry.mValue, *principal, result); + if (result.Failed()) { + NS_WARNING("storage set item failed!"); + } + } + } +} + +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->GetDoc(); + 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)); + } +} diff --git a/toolkit/components/sessionstore/SessionStoreUtils.h b/toolkit/components/sessionstore/SessionStoreUtils.h new file mode 100644 index 0000000000..b52ab4f483 --- /dev/null +++ b/toolkit/components/sessionstore/SessionStoreUtils.h @@ -0,0 +1,101 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/. */ + +#ifndef mozilla_dom_SessionStoreUtils_h +#define mozilla_dom_SessionStoreUtils_h + +#include "mozilla/Attributes.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/SessionStoreUtilsBinding.h" +#include "SessionStoreData.h" + +class nsIDocument; +class nsGlobalWindowInner; + +namespace mozilla { +class ErrorResult; + +namespace dom { + +class GlobalObject; +struct SSScrollPositionDict; + +class SessionStoreUtils { + public: + MOZ_CAN_RUN_SCRIPT + static void ForEachNonDynamicChildFrame( + const GlobalObject& aGlobal, WindowProxyHolder& aWindow, + SessionStoreUtilsFrameCallback& aCallback, ErrorResult& aRv); + + static already_AddRefed<nsISupports> AddDynamicFrameFilteredListener( + const GlobalObject& aGlobal, EventTarget& aTarget, const nsAString& aType, + JS::Handle<JS::Value> aListener, bool aUseCapture, bool aMozSystemGroup, + ErrorResult& aRv); + + static void RemoveDynamicFrameFilteredListener( + const GlobalObject& aGlobal, EventTarget& aTarget, const nsAString& aType, + nsISupports* aListener, bool aUseCapture, bool aMozSystemGroup, + ErrorResult& aRv); + + static void CollectDocShellCapabilities(const GlobalObject& aGlobal, + nsIDocShell* aDocShell, + nsCString& aRetVal); + + static void RestoreDocShellCapabilities( + const GlobalObject& aGlobal, nsIDocShell* aDocShell, + const nsCString& aDisallowCapabilities); + + static void CollectScrollPosition(const GlobalObject& aGlobal, + WindowProxyHolder& aWindow, + Nullable<CollectedData>& aRetVal); + + static void RestoreScrollPosition(const GlobalObject& aGlobal, + nsGlobalWindowInner& aWindow, + const CollectedData& data); + + /* + @param aDocument: DOMDocument instance to obtain form data for. + @param aGeneratedCount: the current number of XPath expressions in the + returned object. + */ + template <typename... ArgsT> + static void CollectFromTextAreaElement(Document& aDocument, + uint16_t& aGeneratedCount, + ArgsT&&... args); + template <typename... ArgsT> + static void CollectFromInputElement(Document& aDocument, + uint16_t& aGeneratedCount, + ArgsT&&... args); + template <typename... ArgsT> + static void CollectFromSelectElement(Document& aDocument, + uint16_t& aGeneratedCount, + ArgsT&&... args); + + static void CollectFormData(const GlobalObject& aGlobal, + WindowProxyHolder& aWindow, + Nullable<CollectedData>& aRetVal); + + MOZ_CAN_RUN_SCRIPT_BOUNDARY + static bool RestoreFormData(const GlobalObject& aGlobal, Document& aDocument, + const CollectedData& aData); + + static void CollectedSessionStorage(BrowsingContext* aBrowsingContext, + nsTArray<nsCString>& aOrigins, + nsTArray<nsString>& aKeys, + nsTArray<nsString>& aValues); + + static void RestoreSessionStorage( + const GlobalObject& aGlobal, nsIDocShell* aDocShell, + const Record<nsString, Record<nsString, nsString>>& aData); + + static void ComposeInputData(const nsTArray<CollectedInputDataValue>& aData, + InputElementData& ret); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_SessionStoreUtils_h diff --git a/toolkit/components/sessionstore/moz.build b/toolkit/components/sessionstore/moz.build new file mode 100644 index 0000000000..dc2b0aec46 --- /dev/null +++ b/toolkit/components/sessionstore/moz.build @@ -0,0 +1,34 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +EXPORTS.mozilla.dom += [ + "SessionStoreData.h", + "SessionStoreListener.h", + "SessionStoreMessageUtils.h", + "SessionStoreUtils.h", +] + +UNIFIED_SOURCES += [ + "SessionStoreListener.cpp", + "SessionStoreUtils.cpp", +] + +EXTRA_JS_MODULES += [ + "SessionStoreFunctions.jsm", +] + +XPIDL_MODULE = "sessionStore_funcs" + +XPIDL_SOURCES += [ + "SessionStoreFunctions.idl", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" + +with Files("**"): + BUG_COMPONENT = ("Firefox", "Session Restore") |