diff options
Diffstat (limited to 'browser/components/sessionstore/SessionHistory.jsm')
-rw-r--r-- | browser/components/sessionstore/SessionHistory.jsm | 428 |
1 files changed, 428 insertions, 0 deletions
diff --git a/browser/components/sessionstore/SessionHistory.jsm b/browser/components/sessionstore/SessionHistory.jsm new file mode 100644 index 000000000..aa9c10379 --- /dev/null +++ b/browser/components/sessionstore/SessionHistory.jsm @@ -0,0 +1,428 @@ +/* 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/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = ["SessionHistory"]; + +const Cu = Components.utils; +const Cc = Components.classes; +const Ci = Components.interfaces; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Utils", + "resource://gre/modules/sessionstore/Utils.jsm"); + +function debug(msg) { + Services.console.logStringMessage("SessionHistory: " + msg); +} + +/** + * The external API exported by this module. + */ +this.SessionHistory = Object.freeze({ + isEmpty: function (docShell) { + return SessionHistoryInternal.isEmpty(docShell); + }, + + collect: function (docShell) { + return SessionHistoryInternal.collect(docShell); + }, + + restore: function (docShell, tabData) { + SessionHistoryInternal.restore(docShell, tabData); + } +}); + +/** + * The internal API for the SessionHistory module. + */ +var SessionHistoryInternal = { + /** + * Returns whether the given docShell's session history is empty. + * + * @param docShell + * The docShell that owns the session history. + */ + isEmpty: function (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. + */ + collect: function (docShell) { + let loadContext = docShell.QueryInterface(Ci.nsILoadContext); + let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation); + let history = webNavigation.sessionHistory.QueryInterface(Ci.nsISHistoryInternal); + + let data = {entries: [], userContextId: loadContext.originAttributes.userContextId }; + + if (history && history.count > 0) { + // Loop over the transaction linked list directly so we can get the + // persist property for each transaction. + for (let txn = history.rootTransaction; txn; txn = txn.next) { + let entry = this.serializeEntry(txn.sHEntry); + entry.persist = txn.persist; + data.entries.push(entry); + } + + // Ensure the index isn't out of bounds if an exception was thrown above. + data.index = Math.min(history.index + 1, data.entries.length); + } + + // 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. + if (data.entries.length == 0) { + let uri = webNavigation.currentURI.spec; + let body = webNavigation.document.body; + // 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" || (body && body.hasChildNodes())) { + data.entries.push({ url: uri }); + data.index = 1; + } + } + + return data; + }, + + /** + * Get an object that is a serialized representation of a History entry. + * + * @param shEntry + * nsISHEntry instance + * @return object + */ + serializeEntry: function (shEntry) { + let entry = { url: shEntry.URI.spec }; + + // Save some bytes and don't include the title property + // if that's identical to the current entry's URL. + if (shEntry.title && shEntry.title != entry.url) { + entry.title = shEntry.title; + } + if (shEntry.isSubFrame) { + entry.subframe = true; + } + + entry.charset = shEntry.URI.originCharset; + + let cacheKey = shEntry.cacheKey; + if (cacheKey && cacheKey instanceof Ci.nsISupportsPRUint32 && + cacheKey.data != 0) { + // XXXbz would be better to have cache keys implement + // nsISerializable or something. + entry.cacheKey = cacheKey.data; + } + entry.ID = shEntry.ID; + entry.docshellID = shEntry.docshellID; + + // 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.referrerURI) { + entry.referrer = shEntry.referrerURI.spec; + entry.referrerPolicy = shEntry.referrerPolicy; + } + + if (shEntry.originalURI) { + entry.originalURI = shEntry.originalURI.spec; + } + + if (shEntry.loadReplace) { + entry.loadReplace = shEntry.loadReplace; + } + + if (shEntry.srcdocData) + entry.srcdocData = shEntry.srcdocData; + + if (shEntry.isSrcdocEntry) + 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; + } + + // Collect triggeringPrincipal data for the current history entry. + // Please note that before Bug 1297338 there was no concept of a + // principalToInherit. To remain backward/forward compatible we + // serialize the principalToInherit as triggeringPrincipal_b64. + // Once principalToInherit is well established (within FF55) + // we can update this code, remove triggeringPrincipal_b64 and + // just keep triggeringPrincipal_base64 as well as + // principalToInherit_base64; see Bug 1301666. + if (shEntry.principalToInherit) { + try { + let principalToInherit = Utils.serializePrincipal(shEntry.principalToInherit); + if (principalToInherit) { + entry.triggeringPrincipal_b64 = principalToInherit; + entry.principalToInherit_base64 = principalToInherit; + } + } catch (e) { + debug(e); + } + } + + if (shEntry.triggeringPrincipal) { + try { + let triggeringPrincipal = Utils.serializePrincipal(shEntry.triggeringPrincipal); + if (triggeringPrincipal) { + entry.triggeringPrincipal_base64 = triggeringPrincipal; + } + } catch (e) { + debug(e); + } + } + + entry.docIdentifier = shEntry.BFCacheEntry.ID; + + if (shEntry.stateData != null) { + entry.structuredCloneState = shEntry.stateData.getDataAsBase64(); + entry.structuredCloneVersion = shEntry.stateData.formatVersion; + } + + if (!(shEntry instanceof Ci.nsISHContainer)) { + return entry; + } + + if (shEntry.childCount > 0 && !shEntry.hasDynamicallyAddedChild()) { + let children = []; + for (let i = 0; i < shEntry.childCount; i++) { + let child = shEntry.GetChildAt(i); + + if (child) { + // Don't try to restore framesets containing wyciwyg URLs. + // (cf. bug 424689 and bug 450595) + if (child.URI.schemeIs("wyciwyg")) { + children.length = 0; + break; + } + + children.push(this.serializeEntry(child)); + } + } + + if (children.length) { + entry.children = children; + } + } + + return entry; + }, + + /** + * Restores session history data for a given docShell. + * + * @param docShell + * The docShell that owns the session history. + * @param tabData + * The tabdata including all history entries. + */ + restore: function (docShell, tabData) { + let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation); + let history = webNavigation.sessionHistory; + if (history.count > 0) { + history.PurgeHistory(history.count); + } + history.QueryInterface(Ci.nsISHistoryInternal); + + 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; + history.addEntry(this.deserializeEntry(entry, idMap, docIdentMap), persist); + } + + // Select the right history entry. + let index = tabData.index - 1; + if (index < history.count && history.index != index) { + history.getEntryAtIndex(index, true); + } + }, + + /** + * 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: function (entry, idMap, docIdentMap) { + + var shEntry = Cc["@mozilla.org/browser/session-history-entry;1"]. + createInstance(Ci.nsISHEntry); + + shEntry.setURI(Utils.makeURI(entry.url, entry.charset)); + shEntry.setTitle(entry.title || entry.url); + if (entry.subframe) + shEntry.setIsSubFrame(entry.subframe || false); + shEntry.loadType = Ci.nsIDocShellLoadInfo.loadHistory; + if (entry.contentType) + shEntry.contentType = entry.contentType; + if (entry.referrer) { + shEntry.referrerURI = Utils.makeURI(entry.referrer); + shEntry.referrerPolicy = entry.referrerPolicy; + } + if (entry.originalURI) { + shEntry.originalURI = Utils.makeURI(entry.originalURI); + } + if (entry.loadReplace) { + shEntry.loadReplace = entry.loadReplace; + } + if (entry.isSrcdocEntry) + shEntry.srcdocData = entry.srcdocData; + if (entry.baseURI) + shEntry.baseURI = Utils.makeURI(entry.baseURI); + + if (entry.cacheKey) { + var cacheKey = Cc["@mozilla.org/supports-PRUint32;1"]. + createInstance(Ci.nsISupportsPRUint32); + cacheKey.data = entry.cacheKey; + shEntry.cacheKey = 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) { + for (id = Date.now(); id in idMap.used; id++); + idMap[entry.ID] = id; + idMap.used[id] = true; + } + shEntry.ID = id; + } + + if (entry.docshellID) + shEntry.docshellID = entry.docshellID; + + if (entry.structuredCloneState && entry.structuredCloneVersion) { + shEntry.stateData = + Cc["@mozilla.org/docshell/structured-clone-container;1"]. + createInstance(Ci.nsIStructuredCloneContainer); + + shEntry.stateData.initFromBase64(entry.structuredCloneState, + entry.structuredCloneVersion); + } + + if (entry.scrollRestorationIsManual) { + shEntry.scrollRestorationIsManual = true; + } else if (entry.scroll) { + var scrollPos = (entry.scroll || "0,0").split(","); + scrollPos = [parseInt(scrollPos[0]) || 0, parseInt(scrollPos[1]) || 0]; + shEntry.setScrollPosition(scrollPos[0], scrollPos[1]); + } + + 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: shEntry, childDocIdents: childDocIdents}; + docIdentMap[entry.docIdentifier] = matchingEntry; + } + else { + shEntry.adoptBFCacheEntry(matchingEntry.shEntry); + childDocIdents = matchingEntry.childDocIdents; + } + } + + // The field entry.owner_b64 got renamed to entry.triggeringPricipal_b64 in + // Bug 1286472. To remain backward compatible we still have to support that + // field for a few cycles before we can remove it within Bug 1289785. + if (entry.owner_b64) { + entry.triggeringPricipal_b64 = entry.owner_b64; + delete entry.owner_b64; + } + + // Before introducing the concept of principalToInherit we only had + // a triggeringPrincipal within every entry which basically is the + // equivalent of the new principalToInherit. To avoid compatibility + // issues, we first check if the entry has entries for + // triggeringPrincipal_base64 and principalToInherit_base64. If not + // we fall back to using the principalToInherit (which is stored + // as triggeringPrincipal_b64) as the triggeringPrincipal and + // the principalToInherit. + // FF55 will remove the triggeringPrincipal_b64, see Bug 1301666. + if (entry.triggeringPrincipal_base64 || entry.principalToInherit_base64) { + if (entry.triggeringPrincipal_base64) { + shEntry.triggeringPrincipal = + Utils.deserializePrincipal(entry.triggeringPrincipal_base64); + } + if (entry.principalToInherit_base64) { + shEntry.principalToInherit = + Utils.deserializePrincipal(entry.principalToInherit_base64); + } + } else if (entry.triggeringPrincipal_b64) { + shEntry.triggeringPrincipal = Utils.deserializePrincipal(entry.triggeringPrincipal_b64); + shEntry.principalToInherit = shEntry.triggeringPrincipal; + } + + if (entry.children && shEntry instanceof Ci.nsISHContainer) { + 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), i); + } + } + + return shEntry; + }, + +}; |