2013-08-29 21:21:52 -07:00
|
|
|
/* 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");
|
|
|
|
|
2013-11-12 15:02:53 -08:00
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "Utils",
|
|
|
|
"resource:///modules/sessionstore/Utils.jsm");
|
2013-08-29 21:21:52 -07:00
|
|
|
|
|
|
|
function debug(msg) {
|
|
|
|
Services.console.logStringMessage("SessionHistory: " + msg);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The external API exported by this module.
|
|
|
|
*/
|
|
|
|
this.SessionHistory = Object.freeze({
|
2014-01-29 05:54:33 -08:00
|
|
|
isEmpty: function (docShell) {
|
|
|
|
return SessionHistoryInternal.isEmpty(docShell);
|
|
|
|
},
|
|
|
|
|
2014-01-14 09:21:48 -08:00
|
|
|
collect: function (docShell) {
|
|
|
|
return SessionHistoryInternal.collect(docShell);
|
2013-11-12 15:02:53 -08:00
|
|
|
},
|
|
|
|
|
|
|
|
restore: function (docShell, tabData) {
|
|
|
|
SessionHistoryInternal.restore(docShell, tabData);
|
2013-08-29 21:21:52 -07:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The internal API for the SessionHistory module.
|
|
|
|
*/
|
|
|
|
let SessionHistoryInternal = {
|
2014-01-29 05:54:33 -08:00
|
|
|
/**
|
|
|
|
* 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;
|
2014-01-31 12:13:23 -08:00
|
|
|
if (!webNavigation.currentURI) {
|
|
|
|
return true;
|
|
|
|
}
|
2014-01-29 05:54:33 -08:00
|
|
|
let uri = webNavigation.currentURI.spec;
|
|
|
|
return uri == "about:blank" && history.count == 0;
|
|
|
|
},
|
|
|
|
|
2013-08-29 21:21:52 -07:00
|
|
|
/**
|
|
|
|
* Collects session history data for a given docShell.
|
|
|
|
*
|
|
|
|
* @param docShell
|
|
|
|
* The docShell that owns the session history.
|
|
|
|
*/
|
2014-01-14 09:21:48 -08:00
|
|
|
collect: function (docShell) {
|
2013-08-29 21:21:52 -07:00
|
|
|
let data = {entries: []};
|
|
|
|
let isPinned = docShell.isAppTab;
|
|
|
|
let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation);
|
2014-10-27 12:27:35 -07:00
|
|
|
let history = webNavigation.sessionHistory.QueryInterface(Ci.nsISHistoryInternal);
|
2013-08-29 21:21:52 -07:00
|
|
|
|
|
|
|
if (history && history.count > 0) {
|
2014-10-27 12:27:35 -07:00
|
|
|
// Loop over the transaction linked list directly so we can get the
|
|
|
|
// persist property for each transaction.
|
2015-04-22 06:51:34 -07:00
|
|
|
for (let txn = history.rootTransaction; txn; txn = txn.next) {
|
|
|
|
let entry = this.serializeEntry(txn.sHEntry, isPinned);
|
2014-10-27 12:27:35 -07:00
|
|
|
entry.persist = txn.persist;
|
|
|
|
data.entries.push(entry);
|
2013-08-29 21:21:52 -07:00
|
|
|
}
|
2013-09-27 17:58:59 -07:00
|
|
|
|
2015-04-22 06:51:34 -07:00
|
|
|
// Ensure the index isn't out of bounds if an exception was thrown above.
|
|
|
|
data.index = Math.min(history.index + 1, data.entries.length);
|
2013-09-30 00:59:14 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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) {
|
2013-08-29 21:21:52 -07:00
|
|
|
let uri = webNavigation.currentURI.spec;
|
2014-02-17 03:35:29 -08:00
|
|
|
let body = webNavigation.document.body;
|
2013-08-29 21:21:52 -07:00
|
|
|
// 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.
|
2014-02-17 03:35:29 -08:00
|
|
|
if (uri != "about:blank" || (body && body.hasChildNodes())) {
|
2013-08-29 21:21:52 -07:00
|
|
|
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
|
|
|
|
* @param isPinned
|
|
|
|
* The tab is pinned and should be treated differently for privacy.
|
|
|
|
* @return object
|
|
|
|
*/
|
2014-01-14 09:21:48 -08:00
|
|
|
serializeEntry: function (shEntry, isPinned) {
|
2013-08-29 21:21:52 -07:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2015-08-28 00:13:03 -07:00
|
|
|
entry.charset = shEntry.URI.originCharset;
|
|
|
|
|
2013-08-29 21:21:52 -07:00
|
|
|
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.
|
2014-11-18 05:46:29 -08:00
|
|
|
if (shEntry.referrerURI) {
|
2013-08-29 21:21:52 -07:00
|
|
|
entry.referrer = shEntry.referrerURI.spec;
|
2014-11-18 05:46:29 -08:00
|
|
|
entry.referrerPolicy = shEntry.referrerPolicy;
|
|
|
|
}
|
2013-08-29 21:21:52 -07:00
|
|
|
|
|
|
|
if (shEntry.srcdocData)
|
|
|
|
entry.srcdocData = shEntry.srcdocData;
|
|
|
|
|
|
|
|
if (shEntry.isSrcdocEntry)
|
|
|
|
entry.isSrcdocEntry = shEntry.isSrcdocEntry;
|
|
|
|
|
2014-02-06 06:46:30 -08:00
|
|
|
if (shEntry.baseURI)
|
2014-02-17 04:45:15 -08:00
|
|
|
entry.baseURI = shEntry.baseURI.spec;
|
2014-02-06 06:46:30 -08:00
|
|
|
|
2013-08-29 21:21:52 -07:00
|
|
|
if (shEntry.contentType)
|
|
|
|
entry.contentType = shEntry.contentType;
|
|
|
|
|
|
|
|
let x = {}, y = {};
|
|
|
|
shEntry.getScrollPosition(x, y);
|
|
|
|
if (x.value != 0 || y.value != 0)
|
|
|
|
entry.scroll = x.value + "," + y.value;
|
|
|
|
|
|
|
|
// Collect owner data for the current history entry.
|
|
|
|
try {
|
2013-11-12 15:02:46 -08:00
|
|
|
let owner = this.serializeOwner(shEntry);
|
2013-08-29 21:21:52 -07:00
|
|
|
if (owner) {
|
|
|
|
entry.owner_b64 = owner;
|
|
|
|
}
|
|
|
|
} catch (ex) {
|
|
|
|
// Not catching anything specific here, just possible errors
|
|
|
|
// from writeCompoundObject() and the like.
|
|
|
|
debug("Failed serializing owner data: " + ex);
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2015-05-25 10:03:33 -07:00
|
|
|
if (shEntry.childCount > 0 && !shEntry.hasDynamicallyAddedChild()) {
|
2013-08-29 21:21:52 -07:00
|
|
|
let children = [];
|
|
|
|
for (let i = 0; i < shEntry.childCount; i++) {
|
|
|
|
let child = shEntry.GetChildAt(i);
|
|
|
|
|
2015-05-25 10:03:33 -07:00
|
|
|
if (child) {
|
2013-08-29 21:21:52 -07:00
|
|
|
// Don't try to restore framesets containing wyciwyg URLs.
|
|
|
|
// (cf. bug 424689 and bug 450595)
|
|
|
|
if (child.URI.schemeIs("wyciwyg")) {
|
|
|
|
children.length = 0;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2014-01-14 09:21:48 -08:00
|
|
|
children.push(this.serializeEntry(child, isPinned));
|
2013-08-29 21:21:52 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (children.length) {
|
|
|
|
entry.children = children;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return entry;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Serialize owner data contained in the given session history entry.
|
|
|
|
*
|
|
|
|
* @param shEntry
|
|
|
|
* The session history entry.
|
|
|
|
* @return The base64 encoded owner data.
|
|
|
|
*/
|
2013-11-12 15:02:46 -08:00
|
|
|
serializeOwner: function (shEntry) {
|
2013-08-29 21:21:52 -07:00
|
|
|
if (!shEntry.owner) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
let binaryStream = Cc["@mozilla.org/binaryoutputstream;1"].
|
|
|
|
createInstance(Ci.nsIObjectOutputStream);
|
|
|
|
let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
|
|
|
|
pipe.init(false, false, 0, 0xffffffff, null);
|
|
|
|
binaryStream.setOutputStream(pipe.outputStream);
|
|
|
|
binaryStream.writeCompoundObject(shEntry.owner, Ci.nsISupports, true);
|
|
|
|
binaryStream.close();
|
|
|
|
|
|
|
|
// Now we want to read the data from the pipe's input end and encode it.
|
|
|
|
let scriptableStream = Cc["@mozilla.org/binaryinputstream;1"].
|
|
|
|
createInstance(Ci.nsIBinaryInputStream);
|
|
|
|
scriptableStream.setInputStream(pipe.inputStream);
|
|
|
|
let ownerBytes =
|
|
|
|
scriptableStream.readByteArray(scriptableStream.available());
|
|
|
|
|
|
|
|
// We can stop doing base64 encoding once our serialization into JSON
|
|
|
|
// is guaranteed to handle all chars in strings, including embedded
|
|
|
|
// nulls.
|
|
|
|
return btoa(String.fromCharCode.apply(null, ownerBytes));
|
2013-11-12 15:02:53 -08:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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++) {
|
2014-10-27 12:27:35 -07:00
|
|
|
let entry = tabData.entries[i];
|
2013-11-12 15:02:53 -08:00
|
|
|
//XXXzpao Wallpaper patch for bug 514751
|
2014-10-27 12:27:35 -07:00
|
|
|
if (!entry.url)
|
2013-11-12 15:02:53 -08:00
|
|
|
continue;
|
2014-10-27 12:27:35 -07:00
|
|
|
let persist = "persist" in entry ? entry.persist : true;
|
|
|
|
history.addEntry(this.deserializeEntry(entry, idMap, docIdentMap), persist);
|
2013-11-12 15:02:53 -08:00
|
|
|
}
|
2015-06-01 10:18:59 -07:00
|
|
|
|
|
|
|
// Select the right history entry.
|
|
|
|
let index = tabData.index - 1;
|
|
|
|
if (index < history.count && history.index != index) {
|
|
|
|
history.getEntryAtIndex(index, true);
|
|
|
|
}
|
2013-11-12 15:02:53 -08:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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);
|
|
|
|
|
2015-08-28 00:13:03 -07:00
|
|
|
shEntry.setURI(Utils.makeURI(entry.url, entry.charset));
|
2013-11-12 15:02:53 -08:00
|
|
|
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;
|
2014-11-18 05:46:29 -08:00
|
|
|
if (entry.referrer) {
|
2013-11-12 15:02:53 -08:00
|
|
|
shEntry.referrerURI = Utils.makeURI(entry.referrer);
|
2014-11-18 05:46:29 -08:00
|
|
|
shEntry.referrerPolicy = entry.referrerPolicy;
|
|
|
|
}
|
2013-11-12 15:02:53 -08:00
|
|
|
if (entry.isSrcdocEntry)
|
|
|
|
shEntry.srcdocData = entry.srcdocData;
|
2014-02-06 06:46:30 -08:00
|
|
|
if (entry.baseURI)
|
2014-02-17 04:45:15 -08:00
|
|
|
shEntry.baseURI = Utils.makeURI(entry.baseURI);
|
2013-11-12 15:02:53 -08:00
|
|
|
|
|
|
|
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.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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (entry.owner_b64) {
|
|
|
|
var ownerInput = Cc["@mozilla.org/io/string-input-stream;1"].
|
|
|
|
createInstance(Ci.nsIStringInputStream);
|
|
|
|
var binaryData = atob(entry.owner_b64);
|
|
|
|
ownerInput.setData(binaryData, binaryData.length);
|
|
|
|
var binaryStream = Cc["@mozilla.org/binaryinputstream;1"].
|
|
|
|
createInstance(Ci.nsIObjectInputStream);
|
|
|
|
binaryStream.setInputStream(ownerInput);
|
|
|
|
try { // Catch possible deserialization exceptions
|
|
|
|
shEntry.owner = binaryStream.readObject(true);
|
|
|
|
} catch (ex) { debug(ex); }
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
},
|
|
|
|
|
2013-08-29 21:21:52 -07:00
|
|
|
};
|