From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- toolkit/modules/sessionstore/PrivacyFilter.sys.mjs | 134 +++++ toolkit/modules/sessionstore/PrivacyLevel.sys.mjs | 54 ++ .../modules/sessionstore/SessionHistory.sys.mjs | 663 +++++++++++++++++++++ toolkit/modules/sessionstore/Utils.sys.mjs | 29 + 4 files changed, 880 insertions(+) create mode 100644 toolkit/modules/sessionstore/PrivacyFilter.sys.mjs create mode 100644 toolkit/modules/sessionstore/PrivacyLevel.sys.mjs create mode 100644 toolkit/modules/sessionstore/SessionHistory.sys.mjs create mode 100644 toolkit/modules/sessionstore/Utils.sys.mjs (limited to 'toolkit/modules/sessionstore') diff --git a/toolkit/modules/sessionstore/PrivacyFilter.sys.mjs b/toolkit/modules/sessionstore/PrivacyFilter.sys.mjs new file mode 100644 index 0000000000..3e7e002a44 --- /dev/null +++ b/toolkit/modules/sessionstore/PrivacyFilter.sys.mjs @@ -0,0 +1,134 @@ +/* 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/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + PrivacyLevel: "resource://gre/modules/sessionstore/PrivacyLevel.sys.mjs", +}); + +/** + * A module that provides methods to filter various kinds of data collected + * from a tab by the current privacy level as set by the user. + */ +export var PrivacyFilter = Object.freeze({ + /** + * Filters the given (serialized) session storage |data| according to the + * current privacy level and returns a new object containing only data that + * we're allowed to store. + * + * @param data The session storage data as collected from a tab. + * @return object + */ + filterSessionStorageData(data) { + let retval = {}; + + for (let host of Object.keys(data)) { + if (lazy.PrivacyLevel.check(host)) { + retval[host] = data[host]; + } + } + + return Object.keys(retval).length ? retval : null; + }, + + /** + * Filters the given (serialized) form |data| according to the current + * privacy level and returns a new object containing only data that we're + * allowed to store. + * + * @param data The form data as collected from a tab. + * @return object + */ + filterFormData(data) { + // If the given form data object has an associated URL that we are not + // allowed to store data for, bail out. We explicitly discard data for any + // children as well even if storing data for those frames would be allowed. + if (!data || (data.url && !lazy.PrivacyLevel.check(data.url))) { + return null; + } + + let retval = {}; + + for (let key of Object.keys(data)) { + if (key === "children") { + let recurse = child => this.filterFormData(child); + let children = data.children.map(recurse).filter(child => child); + + if (children.length) { + retval.children = children; + } + // Only copy keys other than "children" if we have a valid URL in + // data.url and we thus passed the privacy level check. + } else if (data.url) { + retval[key] = data[key]; + } + } + + return Object.keys(retval).length ? retval : null; + }, + + /** + * Removes any private windows and tabs from a given browser state object. + * + * @param browserState (object) + * The browser state for which we remove any private windows and tabs. + * The given object will be modified. + */ + filterPrivateWindowsAndTabs(browserState) { + // Remove private opened windows. + for (let i = browserState.windows.length - 1; i >= 0; i--) { + let win = browserState.windows[i]; + + if (win.isPrivate) { + browserState.windows.splice(i, 1); + + if (browserState.selectedWindow >= i) { + browserState.selectedWindow--; + } + } else { + // Remove private tabs from all open non-private windows. + this.filterPrivateTabs(win); + } + } + + // Remove private closed windows. + browserState._closedWindows = browserState._closedWindows.filter( + win => !win.isPrivate + ); + + // Remove private tabs from all remaining closed windows. + browserState._closedWindows.forEach(win => this.filterPrivateTabs(win)); + }, + + /** + * Removes open private tabs from a given window state object. + * + * @param winState (object) + * The window state for which we remove any private tabs. + * The given object will be modified. + */ + filterPrivateTabs(winState) { + // Remove open private tabs. + for (let i = winState.tabs.length - 1; i >= 0; i--) { + let tab = winState.tabs[i]; + + // Bug 1740261 - We end up with `null` entries in winState.tabs, which if + // we don't check for we end up throwing here. This does not fix the issue of + // how null tabs are getting into the state. + if (!tab || tab.isPrivate) { + winState.tabs.splice(i, 1); + + if (winState.selected >= i) { + winState.selected--; + } + } + } + + // Note that closed private tabs are only stored for private windows. + // There is no need to call this function for private windows as the + // whole window state should just be discarded so we explicitly don't + // try to remove closed private tabs as an optimization. + }, +}); diff --git a/toolkit/modules/sessionstore/PrivacyLevel.sys.mjs b/toolkit/modules/sessionstore/PrivacyLevel.sys.mjs new file mode 100644 index 0000000000..916df7ec67 --- /dev/null +++ b/toolkit/modules/sessionstore/PrivacyLevel.sys.mjs @@ -0,0 +1,54 @@ +/* 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/. */ + +const PREF = "browser.sessionstore.privacy_level"; + +// The following constants represent the different possible privacy levels that +// can be set by the user and that we need to consider when collecting text +// data, and cookies. +// +// Collect data from all sites (http and https). +// const PRIVACY_NONE = 0; +// Collect data from unencrypted sites (http), only. +const PRIVACY_ENCRYPTED = 1; +// Collect no data. +const PRIVACY_FULL = 2; + +/** + * The external API as exposed by this module. + */ +export var PrivacyLevel = Object.freeze({ + /** + * Returns whether the current privacy level allows saving data for the given + * |url|. + * + * @param url The URL we want to save data for. + * @return bool + */ + check(url) { + return PrivacyLevel.canSave(url.startsWith("https:")); + }, + + /** + * Checks whether we're allowed to save data for a specific site. + * + * @param isHttps A boolean that tells whether the site uses TLS. + * @return {bool} Whether we can save data for the specified site. + */ + canSave(isHttps) { + let level = Services.prefs.getIntPref(PREF); + + // Never save any data when full privacy is requested. + if (level == PRIVACY_FULL) { + return false; + } + + // Don't save data for encrypted sites when requested. + if (isHttps && level == PRIVACY_ENCRYPTED) { + return false; + } + + return true; + }, +}); diff --git a/toolkit/modules/sessionstore/SessionHistory.sys.mjs b/toolkit/modules/sessionstore/SessionHistory.sys.mjs new file mode 100644 index 0000000000..19f6210fba --- /dev/null +++ b/toolkit/modules/sessionstore/SessionHistory.sys.mjs @@ -0,0 +1,663 @@ +/* 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/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs", +}); + +/** + * The external API exported by this module. + */ +export var SessionHistory = Object.freeze({ + isEmpty(docShell) { + return SessionHistoryInternal.isEmpty(docShell); + }, + + collect(docShell, aFromIdx = -1) { + if (Services.appinfo.sessionHistoryInParent) { + throw new Error("Use SessionHistory.collectFromParent instead"); + } + return SessionHistoryInternal.collect(docShell, aFromIdx); + }, + + collectFromParent(uri, documentHasChildNodes, history, aFromIdx = -1) { + return SessionHistoryInternal.collectCommon( + uri, + documentHasChildNodes, + history, + aFromIdx + ); + }, + + collectNonWebControlledBlankLoadingSession(browsingContext) { + return SessionHistoryInternal.collectNonWebControlledBlankLoadingSession( + browsingContext + ); + }, + + restore(docShell, tabData) { + if (Services.appinfo.sessionHistoryInParent) { + throw new Error("Use SessionHistory.restoreFromParent instead"); + } + return SessionHistoryInternal.restore( + docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory + .legacySHistory, + tabData + ); + }, + + restoreFromParent(history, tabData) { + return SessionHistoryInternal.restore(history, tabData); + }, +}); + +/** + * The internal API for the SessionHistory module. + */ +var SessionHistoryInternal = { + /** + * Mapping from legacy docshellIDs to docshellUUIDs. + */ + _docshellUUIDMap: new Map(), + + /** + * Returns whether the given docShell's session history is empty. + * + * @param docShell + * The docShell that owns the session history. + */ + isEmpty(docShell) { + let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation); + let history = webNavigation.sessionHistory; + if (!webNavigation.currentURI) { + return true; + } + let uri = webNavigation.currentURI.spec; + return uri == "about:blank" && history.count == 0; + }, + + /** + * Collects session history data for a given docShell. + * + * @param docShell + * The docShell that owns the session history. + * @param aFromIdx + * The starting local index to collect the history from. + * @return An object reprereseting a partial global history update. + */ + collect(docShell, aFromIdx = -1) { + let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation); + let uri = webNavigation.currentURI.displaySpec; + let body = webNavigation.document.body; + let history = webNavigation.sessionHistory; + return this.collectCommon( + uri, + body && body.hasChildNodes(), + history.legacySHistory, + aFromIdx + ); + }, + + collectCommon(uri, documentHasChildNodes, shistory, aFromIdx) { + let data = { + entries: [], + requestedIndex: shistory.requestedIndex + 1, + }; + + // We want to keep track how many entries we *could* have collected and + // how many we skipped, so we can sanitiy-check the current history index + // and also determine whether we need to get any fallback data or not. + let skippedCount = 0, + entryCount = 0; + + if (shistory && shistory.count > 0) { + let count = shistory.count; + for (; entryCount < count; entryCount++) { + let shEntry = shistory.getEntryAtIndex(entryCount); + if (entryCount <= aFromIdx) { + skippedCount++; + continue; + } + let entry = this.serializeEntry(shEntry); + data.entries.push(entry); + } + + // Ensure the index isn't out of bounds if an exception was thrown above. + data.index = Math.min(shistory.index + 1, entryCount); + } + + // If either the session history isn't available yet or doesn't have any + // valid entries, make sure we at least include the current page, + // unless of course we just skipped all entries because aFromIdx was big enough. + if (!data.entries.length && (skippedCount != entryCount || aFromIdx < 0)) { + // We landed here because the history is inaccessible or there are no + // history entries. In that case we should at least record the docShell's + // current URL as a single history entry. If the URL is not about:blank + // or it's a blank tab that was modified (like a custom newtab page), + // record it. For about:blank we explicitly want an empty array without + // an 'index' property to denote that there are no history entries. + if (uri != "about:blank" || documentHasChildNodes) { + data.entries.push({ + url: uri, + triggeringPrincipal_base64: lazy.E10SUtils.SERIALIZED_SYSTEMPRINCIPAL, + }); + data.index = 1; + } + } + + data.fromIdx = aFromIdx; + + return data; + }, + + collectNonWebControlledBlankLoadingSession(browsingContext) { + if ( + browsingContext.sessionHistory?.count === 0 && + browsingContext.nonWebControlledBlankURI && + browsingContext.mostRecentLoadingSessionHistoryEntry + ) { + return { + entries: [ + this.serializeEntry( + browsingContext.mostRecentLoadingSessionHistoryEntry + ), + ], + // Set 1 to the index, as the array of session entries is 1-based. + index: 1, + fromIdx: -1, + requestedIndex: browsingContext.sessionHistory.requestedIndex + 1, + }; + } + + return null; + }, + + /** + * Get an object that is a serialized representation of a History entry. + * + * @param shEntry + * nsISHEntry instance + * @return object + */ + serializeEntry(shEntry) { + let entry = { url: shEntry.URI.displaySpec, title: shEntry.title }; + + if (shEntry.isSubFrame) { + entry.subframe = true; + } + + entry.cacheKey = shEntry.cacheKey; + entry.ID = shEntry.ID; + entry.docshellUUID = shEntry.docshellID.toString(); + + // We will include the property only if it's truthy to save a couple of + // bytes when the resulting object is stringified and saved to disk. + if (shEntry.referrerInfo) { + entry.referrerInfo = lazy.E10SUtils.serializeReferrerInfo( + shEntry.referrerInfo + ); + } + + if (shEntry.originalURI) { + entry.originalURI = shEntry.originalURI.spec; + } + + if (shEntry.resultPrincipalURI) { + entry.resultPrincipalURI = shEntry.resultPrincipalURI.spec; + + // For downgrade compatibility we store the loadReplace property as it + // would be stored before result principal URI introduction so that + // the old code can still create URL based principals for channels + // correctly. When resultPrincipalURI is non-null and not equal to + // channel's orignalURI in the new code, it's equal to setting + // LOAD_REPLACE in the old code. Note that we only do 'the best we can' + // here to derivate the 'old' loadReplace flag value. + entry.loadReplace = entry.resultPrincipalURI != entry.originalURI; + } else { + // We want to store the property to let the backward compatibility code, + // when reading the stored session, work. When this property is undefined + // that code will derive the result principal URI from the load replace + // flag. + entry.resultPrincipalURI = null; + } + + if (shEntry.loadReplace) { + // Storing under a new property name, since it has changed its meaning + // with the result principal URI introduction. + entry.loadReplace2 = shEntry.loadReplace; + } + + if (shEntry.isSrcdocEntry) { + entry.srcdocData = shEntry.srcdocData; + entry.isSrcdocEntry = shEntry.isSrcdocEntry; + } + + if (shEntry.baseURI) { + entry.baseURI = shEntry.baseURI.spec; + } + + if (shEntry.contentType) { + entry.contentType = shEntry.contentType; + } + + if (shEntry.scrollRestorationIsManual) { + entry.scrollRestorationIsManual = true; + } else { + let x = {}, + y = {}; + shEntry.getScrollPosition(x, y); + if (x.value !== 0 || y.value !== 0) { + entry.scroll = x.value + "," + y.value; + } + + let layoutHistoryState = shEntry.layoutHistoryState; + if (layoutHistoryState && layoutHistoryState.hasStates) { + let presStates = layoutHistoryState + .getKeys() + .map(key => this._getSerializablePresState(layoutHistoryState, key)) + .filter( + presState => + // Only keep presState entries that contain more than the key itself. + Object.getOwnPropertyNames(presState).length > 1 + ); + + if (presStates.length) { + entry.presState = presStates; + } + } + } + + // Collect triggeringPrincipal data for the current history entry. + if (shEntry.principalToInherit) { + entry.principalToInherit_base64 = lazy.E10SUtils.serializePrincipal( + shEntry.principalToInherit + ); + } + + if (shEntry.partitionedPrincipalToInherit) { + entry.partitionedPrincipalToInherit_base64 = + lazy.E10SUtils.serializePrincipal( + shEntry.partitionedPrincipalToInherit + ); + } + + entry.hasUserInteraction = shEntry.hasUserInteraction; + + if (shEntry.triggeringPrincipal) { + entry.triggeringPrincipal_base64 = lazy.E10SUtils.serializePrincipal( + shEntry.triggeringPrincipal + ); + } + + if (shEntry.csp) { + entry.csp = lazy.E10SUtils.serializeCSP(shEntry.csp); + } + + entry.docIdentifier = shEntry.bfcacheID; + + if (shEntry.stateData != null) { + let stateData = shEntry.stateData; + entry.structuredCloneState = stateData.getDataAsBase64(); + entry.structuredCloneVersion = stateData.formatVersion; + } + + if (shEntry.wireframe != null) { + entry.wireframe = shEntry.wireframe; + } + + if (shEntry.childCount > 0 && !shEntry.hasDynamicallyAddedChild()) { + let children = []; + for (let i = 0; i < shEntry.childCount; i++) { + let child = shEntry.GetChildAt(i); + + if (child) { + children.push(this.serializeEntry(child)); + } + } + + if (children.length) { + entry.children = children; + } + } + + entry.persist = shEntry.persist; + + return entry; + }, + + /** + * Get an object that is a serializable representation of a PresState. + * + * @param layoutHistoryState + * nsILayoutHistoryState instance + * @param stateKey + * The state key of the presState to be retrieved. + * @return object + */ + _getSerializablePresState(layoutHistoryState, stateKey) { + let presState = { stateKey }; + let x = {}, + y = {}, + scrollOriginDowngrade = {}, + res = {}; + + layoutHistoryState.getPresState(stateKey, x, y, scrollOriginDowngrade, res); + if (x.value !== 0 || y.value !== 0) { + presState.scroll = x.value + "," + y.value; + } + if (scrollOriginDowngrade.value === false) { + presState.scrollOriginDowngrade = scrollOriginDowngrade.value; + } + if (res.value != 1.0) { + presState.res = res.value; + } + + return presState; + }, + + /** + * Restores session history data for a given docShell. + * + * @param history + * The session history object. + * @param tabData + * The tabdata including all history entries. + * @return A reference to the docShell's nsISHistory interface. + */ + restore(history, tabData) { + if (history.count > 0) { + history.purgeHistory(history.count); + } + + let idMap = { used: {} }; + let docIdentMap = {}; + for (let i = 0; i < tabData.entries.length; i++) { + let entry = tabData.entries[i]; + // XXXzpao Wallpaper patch for bug 514751 + if (!entry.url) { + continue; + } + let persist = "persist" in entry ? entry.persist : true; + let shEntry = this.deserializeEntry(entry, idMap, docIdentMap, history); + + // To enable a smooth migration, we treat values of null/undefined as having + // user interaction (because we don't want to hide all session history that was + // added before we started recording user interaction). + // + // This attribute is only set on top-level SH history entries, so we set it + // outside of deserializeEntry since that is called recursively. + if (entry.hasUserInteraction == undefined) { + shEntry.hasUserInteraction = true; + } else { + shEntry.hasUserInteraction = entry.hasUserInteraction; + } + + history.addEntry(shEntry, persist); + } + + // Select the right history entry. + let index = tabData.index - 1; + if (index < history.count && history.index != index) { + history.index = index; + } + return history; + }, + + /** + * Expands serialized history data into a session-history-entry instance. + * + * @param entry + * Object containing serialized history data for a URL + * @param idMap + * Hash for ensuring unique frame IDs + * @param docIdentMap + * Hash to ensure reuse of BFCache entries + * @returns nsISHEntry + */ + deserializeEntry(entry, idMap, docIdentMap, shistory) { + var shEntry = shistory.createEntry(); + + shEntry.URI = Services.io.newURI(entry.url); + shEntry.title = entry.title || entry.url; + if (entry.subframe) { + shEntry.isSubFrame = entry.subframe || false; + } + shEntry.setLoadTypeAsHistory(); + if (entry.contentType) { + shEntry.contentType = entry.contentType; + } + // Referrer information is now stored as a referrerInfo property. We should + // also cope with the old format of passing `referrer` and `referrerPolicy` + // separately. + if (entry.referrerInfo) { + shEntry.referrerInfo = lazy.E10SUtils.deserializeReferrerInfo( + entry.referrerInfo + ); + } else if (entry.referrer) { + let ReferrerInfo = Components.Constructor( + "@mozilla.org/referrer-info;1", + "nsIReferrerInfo", + "init" + ); + shEntry.referrerInfo = new ReferrerInfo( + entry.referrerPolicy, + true, + Services.io.newURI(entry.referrer) + ); + } + + if (entry.originalURI) { + shEntry.originalURI = Services.io.newURI(entry.originalURI); + } + if (typeof entry.resultPrincipalURI === "undefined" && entry.loadReplace) { + // This is backward compatibility code for stored sessions saved prior to + // introduction of the resultPrincipalURI property. The equivalent of this + // property non-null value used to be the URL while the LOAD_REPLACE flag + // was set. + shEntry.resultPrincipalURI = shEntry.URI; + } else if (entry.resultPrincipalURI) { + shEntry.resultPrincipalURI = Services.io.newURI(entry.resultPrincipalURI); + } + if (entry.loadReplace2) { + shEntry.loadReplace = entry.loadReplace2; + } + if (entry.isSrcdocEntry) { + shEntry.srcdocData = entry.srcdocData; + } + if (entry.baseURI) { + shEntry.baseURI = Services.io.newURI(entry.baseURI); + } + + if (entry.cacheKey) { + shEntry.cacheKey = entry.cacheKey; + } + + if (entry.ID) { + // get a new unique ID for this frame (since the one from the last + // start might already be in use) + var id = idMap[entry.ID] || 0; + if (!id) { + // eslint-disable-next-line no-empty + for (id = Date.now(); id in idMap.used; id++) {} + idMap[entry.ID] = id; + idMap.used[id] = true; + } + shEntry.ID = id; + } + + // If we have the legacy docshellID on our entry, upgrade it to a + // docshellUUID by going through the mapping. + if (entry.docshellID) { + if (!this._docshellUUIDMap.has(entry.docshellID)) { + // Convert the nsID to a string so that the docshellUUID property + // is correctly stored as a string. + this._docshellUUIDMap.set( + entry.docshellID, + Services.uuid.generateUUID().toString() + ); + } + entry.docshellUUID = this._docshellUUIDMap.get(entry.docshellID); + delete entry.docshellID; + } + + if (entry.docshellUUID) { + shEntry.docshellID = Components.ID(entry.docshellUUID); + } + + if (entry.structuredCloneState && entry.structuredCloneVersion) { + var stateData = Cc[ + "@mozilla.org/docshell/structured-clone-container;1" + ].createInstance(Ci.nsIStructuredCloneContainer); + + stateData.initFromBase64( + entry.structuredCloneState, + entry.structuredCloneVersion + ); + shEntry.stateData = stateData; + } + + if (entry.scrollRestorationIsManual) { + shEntry.scrollRestorationIsManual = true; + } else { + if (entry.scroll) { + shEntry.setScrollPosition( + ...this._deserializeScrollPosition(entry.scroll) + ); + } + + if (entry.presState) { + let layoutHistoryState = shEntry.initLayoutHistoryState(); + + for (let presState of entry.presState) { + this._deserializePresState(layoutHistoryState, presState); + } + } + } + + let childDocIdents = {}; + if (entry.docIdentifier) { + // If we have a serialized document identifier, try to find an SHEntry + // which matches that doc identifier and adopt that SHEntry's + // BFCacheEntry. If we don't find a match, insert shEntry as the match + // for the document identifier. + let matchingEntry = docIdentMap[entry.docIdentifier]; + if (!matchingEntry) { + matchingEntry = { shEntry, childDocIdents }; + docIdentMap[entry.docIdentifier] = matchingEntry; + } else { + shEntry.adoptBFCacheEntry(matchingEntry.shEntry); + childDocIdents = matchingEntry.childDocIdents; + } + } + + // Every load must have a triggeringPrincipal to load otherwise we prevent it, + // this code *must* always return a valid principal: + shEntry.triggeringPrincipal = lazy.E10SUtils.deserializePrincipal( + entry.triggeringPrincipal_base64, + () => { + // This callback fires when we failed to deserialize the principal (or we don't have one) + // and this ensures we always have a principal returned from this function. + // We must always have a triggering principal for a load to work. + // A null principal won't always work however is safe to use. + console.warn( + "Couldn't deserialize the triggeringPrincipal, falling back to NullPrincipal" + ); + return Services.scriptSecurityManager.createNullPrincipal({}); + } + ); + // As both partitionedPrincipal and principalToInherit are both not required to load + // it's ok to keep these undefined when we don't have a previously defined principal. + if (entry.partitionedPrincipalToInherit_base64) { + shEntry.partitionedPrincipalToInherit = + lazy.E10SUtils.deserializePrincipal( + entry.partitionedPrincipalToInherit_base64 + ); + } + if (entry.principalToInherit_base64) { + shEntry.principalToInherit = lazy.E10SUtils.deserializePrincipal( + entry.principalToInherit_base64 + ); + } + if (entry.csp) { + shEntry.csp = lazy.E10SUtils.deserializeCSP(entry.csp); + } + if (entry.wireframe) { + shEntry.wireframe = entry.wireframe; + } + + if (entry.children) { + for (var i = 0; i < entry.children.length; i++) { + // XXXzpao Wallpaper patch for bug 514751 + if (!entry.children[i].url) { + continue; + } + + // We're getting sessionrestore.js files with a cycle in the + // doc-identifier graph, likely due to bug 698656. (That is, we have + // an entry where doc identifier A is an ancestor of doc identifier B, + // and another entry where doc identifier B is an ancestor of A.) + // + // If we were to respect these doc identifiers, we'd create a cycle in + // the SHEntries themselves, which causes the docshell to loop forever + // when it looks for the root SHEntry. + // + // So as a hack to fix this, we restrict the scope of a doc identifier + // to be a node's siblings and cousins, and pass childDocIdents, not + // aDocIdents, to _deserializeHistoryEntry. That is, we say that two + // SHEntries with the same doc identifier have the same document iff + // they have the same parent or their parents have the same document. + + shEntry.AddChild( + this.deserializeEntry( + entry.children[i], + idMap, + childDocIdents, + shistory + ), + i + ); + } + } + + return shEntry; + }, + + /** + * Expands serialized PresState data and adds it to the given nsILayoutHistoryState. + * + * @param layoutHistoryState + * nsILayoutHistoryState instance + * @param presState + * Object containing serialized PresState data. + */ + _deserializePresState(layoutHistoryState, presState) { + let stateKey = presState.stateKey; + let scrollOriginDowngrade = + typeof presState.scrollOriginDowngrade == "boolean" + ? presState.scrollOriginDowngrade + : true; + let res = presState.res || 1.0; + + layoutHistoryState.addNewPresState( + stateKey, + ...this._deserializeScrollPosition(presState.scroll), + scrollOriginDowngrade, + res + ); + }, + + /** + * Expands serialized scroll position data into an array containing the x and y coordinates, + * defaulting to 0,0 if no scroll position was found. + * + * @param scroll + * Object containing serialized scroll position data. + * @return An array containing the scroll position's x and y coordinates. + */ + _deserializeScrollPosition(scroll = "0,0") { + return scroll.split(",").map(pos => parseInt(pos, 10) || 0); + }, +}; diff --git a/toolkit/modules/sessionstore/Utils.sys.mjs b/toolkit/modules/sessionstore/Utils.sys.mjs new file mode 100644 index 0000000000..627cd22686 --- /dev/null +++ b/toolkit/modules/sessionstore/Utils.sys.mjs @@ -0,0 +1,29 @@ +/* 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/. */ + +export var Utils = Object.freeze({ + /** + * Restores frame tree |data|, starting at the given root |frame|. As the + * function recurses into descendant frames it will call cb(frame, data) for + * each frame it encounters, starting with the given root. + */ + restoreFrameTreeData(frame, data, cb) { + // Restore data for the root frame. + // The callback can abort by returning false. + if (cb(frame, data) === false) { + return; + } + + if (!data.hasOwnProperty("children")) { + return; + } + + // Recurse into child frames. + SessionStoreUtils.forEachNonDynamicChildFrame(frame, (subframe, index) => { + if (data.children[index]) { + this.restoreFrameTreeData(subframe, data.children[index], cb); + } + }); + }, +}); -- cgit v1.2.3