summaryrefslogtreecommitdiffstats
path: root/toolkit/modules/sessionstore/SessionHistory.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--toolkit/modules/sessionstore/SessionHistory.sys.mjs663
1 files changed, 663 insertions, 0 deletions
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);
+ },
+};