1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/components/sessionstore/src/SessionHistory.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,434 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 +* License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.6 +* You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +this.EXPORTED_SYMBOLS = ["SessionHistory"]; 1.11 + 1.12 +const Cu = Components.utils; 1.13 +const Cc = Components.classes; 1.14 +const Ci = Components.interfaces; 1.15 + 1.16 +Cu.import("resource://gre/modules/Services.jsm"); 1.17 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.18 + 1.19 +XPCOMUtils.defineLazyModuleGetter(this, "Utils", 1.20 + "resource:///modules/sessionstore/Utils.jsm"); 1.21 + 1.22 +function debug(msg) { 1.23 + Services.console.logStringMessage("SessionHistory: " + msg); 1.24 +} 1.25 + 1.26 +/** 1.27 + * The external API exported by this module. 1.28 + */ 1.29 +this.SessionHistory = Object.freeze({ 1.30 + isEmpty: function (docShell) { 1.31 + return SessionHistoryInternal.isEmpty(docShell); 1.32 + }, 1.33 + 1.34 + collect: function (docShell) { 1.35 + return SessionHistoryInternal.collect(docShell); 1.36 + }, 1.37 + 1.38 + restore: function (docShell, tabData) { 1.39 + SessionHistoryInternal.restore(docShell, tabData); 1.40 + } 1.41 +}); 1.42 + 1.43 +/** 1.44 + * The internal API for the SessionHistory module. 1.45 + */ 1.46 +let SessionHistoryInternal = { 1.47 + /** 1.48 + * Returns whether the given docShell's session history is empty. 1.49 + * 1.50 + * @param docShell 1.51 + * The docShell that owns the session history. 1.52 + */ 1.53 + isEmpty: function (docShell) { 1.54 + let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation); 1.55 + let history = webNavigation.sessionHistory; 1.56 + if (!webNavigation.currentURI) { 1.57 + return true; 1.58 + } 1.59 + let uri = webNavigation.currentURI.spec; 1.60 + return uri == "about:blank" && history.count == 0; 1.61 + }, 1.62 + 1.63 + /** 1.64 + * Collects session history data for a given docShell. 1.65 + * 1.66 + * @param docShell 1.67 + * The docShell that owns the session history. 1.68 + */ 1.69 + collect: function (docShell) { 1.70 + let data = {entries: []}; 1.71 + let isPinned = docShell.isAppTab; 1.72 + let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation); 1.73 + let history = webNavigation.sessionHistory; 1.74 + 1.75 + if (history && history.count > 0) { 1.76 + let oldest; 1.77 + let maxSerializeBack = 1.78 + Services.prefs.getIntPref("browser.sessionstore.max_serialize_back"); 1.79 + if (maxSerializeBack >= 0) { 1.80 + oldest = Math.max(0, history.index - maxSerializeBack); 1.81 + } else { // History.getEntryAtIndex(0, ...) is the oldest. 1.82 + oldest = 0; 1.83 + } 1.84 + 1.85 + let newest; 1.86 + let maxSerializeFwd = 1.87 + Services.prefs.getIntPref("browser.sessionstore.max_serialize_forward"); 1.88 + if (maxSerializeFwd >= 0) { 1.89 + newest = Math.min(history.count - 1, history.index + maxSerializeFwd); 1.90 + } else { // History.getEntryAtIndex(history.count - 1, ...) is the newest. 1.91 + newest = history.count - 1; 1.92 + } 1.93 + 1.94 + try { 1.95 + for (let i = oldest; i <= newest; i++) { 1.96 + let shEntry = history.getEntryAtIndex(i, false); 1.97 + let entry = this.serializeEntry(shEntry, isPinned); 1.98 + data.entries.push(entry); 1.99 + } 1.100 + } catch (ex) { 1.101 + // In some cases, getEntryAtIndex will throw. This seems to be due to 1.102 + // history.count being higher than it should be. By doing this in a 1.103 + // try-catch, we'll update history to where it breaks, print an error 1.104 + // message, and still save sessionstore.js. 1.105 + debug("SessionStore failed gathering complete history " + 1.106 + "for the focused window/tab. See bug 669196."); 1.107 + } 1.108 + 1.109 + // Set the one-based index of the currently active tab, 1.110 + // ensuring it isn't out of bounds if an exception was thrown above. 1.111 + data.index = Math.min(history.index - oldest + 1, data.entries.length); 1.112 + } 1.113 + 1.114 + // If either the session history isn't available yet or doesn't have any 1.115 + // valid entries, make sure we at least include the current page. 1.116 + if (data.entries.length == 0) { 1.117 + let uri = webNavigation.currentURI.spec; 1.118 + let body = webNavigation.document.body; 1.119 + // We landed here because the history is inaccessible or there are no 1.120 + // history entries. In that case we should at least record the docShell's 1.121 + // current URL as a single history entry. If the URL is not about:blank 1.122 + // or it's a blank tab that was modified (like a custom newtab page), 1.123 + // record it. For about:blank we explicitly want an empty array without 1.124 + // an 'index' property to denote that there are no history entries. 1.125 + if (uri != "about:blank" || (body && body.hasChildNodes())) { 1.126 + data.entries.push({ url: uri }); 1.127 + data.index = 1; 1.128 + } 1.129 + } 1.130 + 1.131 + return data; 1.132 + }, 1.133 + 1.134 + /** 1.135 + * Determines whether a given session history entry has been added dynamically. 1.136 + * 1.137 + * @param shEntry 1.138 + * The session history entry. 1.139 + * @return bool 1.140 + */ 1.141 + isDynamic: function (shEntry) { 1.142 + // shEntry.isDynamicallyAdded() is true for dynamically added 1.143 + // <iframe> and <frameset>, but also for <html> (the root of the 1.144 + // document) so we use shEntry.parent to ensure that we're not looking 1.145 + // at the root of the document 1.146 + return shEntry.parent && shEntry.isDynamicallyAdded(); 1.147 + }, 1.148 + 1.149 + /** 1.150 + * Get an object that is a serialized representation of a History entry. 1.151 + * 1.152 + * @param shEntry 1.153 + * nsISHEntry instance 1.154 + * @param isPinned 1.155 + * The tab is pinned and should be treated differently for privacy. 1.156 + * @return object 1.157 + */ 1.158 + serializeEntry: function (shEntry, isPinned) { 1.159 + let entry = { url: shEntry.URI.spec }; 1.160 + 1.161 + // Save some bytes and don't include the title property 1.162 + // if that's identical to the current entry's URL. 1.163 + if (shEntry.title && shEntry.title != entry.url) { 1.164 + entry.title = shEntry.title; 1.165 + } 1.166 + if (shEntry.isSubFrame) { 1.167 + entry.subframe = true; 1.168 + } 1.169 + 1.170 + let cacheKey = shEntry.cacheKey; 1.171 + if (cacheKey && cacheKey instanceof Ci.nsISupportsPRUint32 && 1.172 + cacheKey.data != 0) { 1.173 + // XXXbz would be better to have cache keys implement 1.174 + // nsISerializable or something. 1.175 + entry.cacheKey = cacheKey.data; 1.176 + } 1.177 + entry.ID = shEntry.ID; 1.178 + entry.docshellID = shEntry.docshellID; 1.179 + 1.180 + // We will include the property only if it's truthy to save a couple of 1.181 + // bytes when the resulting object is stringified and saved to disk. 1.182 + if (shEntry.referrerURI) 1.183 + entry.referrer = shEntry.referrerURI.spec; 1.184 + 1.185 + if (shEntry.srcdocData) 1.186 + entry.srcdocData = shEntry.srcdocData; 1.187 + 1.188 + if (shEntry.isSrcdocEntry) 1.189 + entry.isSrcdocEntry = shEntry.isSrcdocEntry; 1.190 + 1.191 + if (shEntry.baseURI) 1.192 + entry.baseURI = shEntry.baseURI.spec; 1.193 + 1.194 + if (shEntry.contentType) 1.195 + entry.contentType = shEntry.contentType; 1.196 + 1.197 + let x = {}, y = {}; 1.198 + shEntry.getScrollPosition(x, y); 1.199 + if (x.value != 0 || y.value != 0) 1.200 + entry.scroll = x.value + "," + y.value; 1.201 + 1.202 + // Collect owner data for the current history entry. 1.203 + try { 1.204 + let owner = this.serializeOwner(shEntry); 1.205 + if (owner) { 1.206 + entry.owner_b64 = owner; 1.207 + } 1.208 + } catch (ex) { 1.209 + // Not catching anything specific here, just possible errors 1.210 + // from writeCompoundObject() and the like. 1.211 + debug("Failed serializing owner data: " + ex); 1.212 + } 1.213 + 1.214 + entry.docIdentifier = shEntry.BFCacheEntry.ID; 1.215 + 1.216 + if (shEntry.stateData != null) { 1.217 + entry.structuredCloneState = shEntry.stateData.getDataAsBase64(); 1.218 + entry.structuredCloneVersion = shEntry.stateData.formatVersion; 1.219 + } 1.220 + 1.221 + if (!(shEntry instanceof Ci.nsISHContainer)) { 1.222 + return entry; 1.223 + } 1.224 + 1.225 + if (shEntry.childCount > 0) { 1.226 + let children = []; 1.227 + for (let i = 0; i < shEntry.childCount; i++) { 1.228 + let child = shEntry.GetChildAt(i); 1.229 + 1.230 + if (child && !this.isDynamic(child)) { 1.231 + // Don't try to restore framesets containing wyciwyg URLs. 1.232 + // (cf. bug 424689 and bug 450595) 1.233 + if (child.URI.schemeIs("wyciwyg")) { 1.234 + children.length = 0; 1.235 + break; 1.236 + } 1.237 + 1.238 + children.push(this.serializeEntry(child, isPinned)); 1.239 + } 1.240 + } 1.241 + 1.242 + if (children.length) { 1.243 + entry.children = children; 1.244 + } 1.245 + } 1.246 + 1.247 + return entry; 1.248 + }, 1.249 + 1.250 + /** 1.251 + * Serialize owner data contained in the given session history entry. 1.252 + * 1.253 + * @param shEntry 1.254 + * The session history entry. 1.255 + * @return The base64 encoded owner data. 1.256 + */ 1.257 + serializeOwner: function (shEntry) { 1.258 + if (!shEntry.owner) { 1.259 + return null; 1.260 + } 1.261 + 1.262 + let binaryStream = Cc["@mozilla.org/binaryoutputstream;1"]. 1.263 + createInstance(Ci.nsIObjectOutputStream); 1.264 + let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe); 1.265 + pipe.init(false, false, 0, 0xffffffff, null); 1.266 + binaryStream.setOutputStream(pipe.outputStream); 1.267 + binaryStream.writeCompoundObject(shEntry.owner, Ci.nsISupports, true); 1.268 + binaryStream.close(); 1.269 + 1.270 + // Now we want to read the data from the pipe's input end and encode it. 1.271 + let scriptableStream = Cc["@mozilla.org/binaryinputstream;1"]. 1.272 + createInstance(Ci.nsIBinaryInputStream); 1.273 + scriptableStream.setInputStream(pipe.inputStream); 1.274 + let ownerBytes = 1.275 + scriptableStream.readByteArray(scriptableStream.available()); 1.276 + 1.277 + // We can stop doing base64 encoding once our serialization into JSON 1.278 + // is guaranteed to handle all chars in strings, including embedded 1.279 + // nulls. 1.280 + return btoa(String.fromCharCode.apply(null, ownerBytes)); 1.281 + }, 1.282 + 1.283 + /** 1.284 + * Restores session history data for a given docShell. 1.285 + * 1.286 + * @param docShell 1.287 + * The docShell that owns the session history. 1.288 + * @param tabData 1.289 + * The tabdata including all history entries. 1.290 + */ 1.291 + restore: function (docShell, tabData) { 1.292 + let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation); 1.293 + let history = webNavigation.sessionHistory; 1.294 + 1.295 + if (history.count > 0) { 1.296 + history.PurgeHistory(history.count); 1.297 + } 1.298 + history.QueryInterface(Ci.nsISHistoryInternal); 1.299 + 1.300 + let idMap = { used: {} }; 1.301 + let docIdentMap = {}; 1.302 + for (let i = 0; i < tabData.entries.length; i++) { 1.303 + //XXXzpao Wallpaper patch for bug 514751 1.304 + if (!tabData.entries[i].url) 1.305 + continue; 1.306 + history.addEntry(this.deserializeEntry(tabData.entries[i], 1.307 + idMap, docIdentMap), true); 1.308 + } 1.309 + }, 1.310 + 1.311 + /** 1.312 + * Expands serialized history data into a session-history-entry instance. 1.313 + * 1.314 + * @param entry 1.315 + * Object containing serialized history data for a URL 1.316 + * @param idMap 1.317 + * Hash for ensuring unique frame IDs 1.318 + * @param docIdentMap 1.319 + * Hash to ensure reuse of BFCache entries 1.320 + * @returns nsISHEntry 1.321 + */ 1.322 + deserializeEntry: function (entry, idMap, docIdentMap) { 1.323 + 1.324 + var shEntry = Cc["@mozilla.org/browser/session-history-entry;1"]. 1.325 + createInstance(Ci.nsISHEntry); 1.326 + 1.327 + shEntry.setURI(Utils.makeURI(entry.url)); 1.328 + shEntry.setTitle(entry.title || entry.url); 1.329 + if (entry.subframe) 1.330 + shEntry.setIsSubFrame(entry.subframe || false); 1.331 + shEntry.loadType = Ci.nsIDocShellLoadInfo.loadHistory; 1.332 + if (entry.contentType) 1.333 + shEntry.contentType = entry.contentType; 1.334 + if (entry.referrer) 1.335 + shEntry.referrerURI = Utils.makeURI(entry.referrer); 1.336 + if (entry.isSrcdocEntry) 1.337 + shEntry.srcdocData = entry.srcdocData; 1.338 + if (entry.baseURI) 1.339 + shEntry.baseURI = Utils.makeURI(entry.baseURI); 1.340 + 1.341 + if (entry.cacheKey) { 1.342 + var cacheKey = Cc["@mozilla.org/supports-PRUint32;1"]. 1.343 + createInstance(Ci.nsISupportsPRUint32); 1.344 + cacheKey.data = entry.cacheKey; 1.345 + shEntry.cacheKey = cacheKey; 1.346 + } 1.347 + 1.348 + if (entry.ID) { 1.349 + // get a new unique ID for this frame (since the one from the last 1.350 + // start might already be in use) 1.351 + var id = idMap[entry.ID] || 0; 1.352 + if (!id) { 1.353 + for (id = Date.now(); id in idMap.used; id++); 1.354 + idMap[entry.ID] = id; 1.355 + idMap.used[id] = true; 1.356 + } 1.357 + shEntry.ID = id; 1.358 + } 1.359 + 1.360 + if (entry.docshellID) 1.361 + shEntry.docshellID = entry.docshellID; 1.362 + 1.363 + if (entry.structuredCloneState && entry.structuredCloneVersion) { 1.364 + shEntry.stateData = 1.365 + Cc["@mozilla.org/docshell/structured-clone-container;1"]. 1.366 + createInstance(Ci.nsIStructuredCloneContainer); 1.367 + 1.368 + shEntry.stateData.initFromBase64(entry.structuredCloneState, 1.369 + entry.structuredCloneVersion); 1.370 + } 1.371 + 1.372 + if (entry.scroll) { 1.373 + var scrollPos = (entry.scroll || "0,0").split(","); 1.374 + scrollPos = [parseInt(scrollPos[0]) || 0, parseInt(scrollPos[1]) || 0]; 1.375 + shEntry.setScrollPosition(scrollPos[0], scrollPos[1]); 1.376 + } 1.377 + 1.378 + let childDocIdents = {}; 1.379 + if (entry.docIdentifier) { 1.380 + // If we have a serialized document identifier, try to find an SHEntry 1.381 + // which matches that doc identifier and adopt that SHEntry's 1.382 + // BFCacheEntry. If we don't find a match, insert shEntry as the match 1.383 + // for the document identifier. 1.384 + let matchingEntry = docIdentMap[entry.docIdentifier]; 1.385 + if (!matchingEntry) { 1.386 + matchingEntry = {shEntry: shEntry, childDocIdents: childDocIdents}; 1.387 + docIdentMap[entry.docIdentifier] = matchingEntry; 1.388 + } 1.389 + else { 1.390 + shEntry.adoptBFCacheEntry(matchingEntry.shEntry); 1.391 + childDocIdents = matchingEntry.childDocIdents; 1.392 + } 1.393 + } 1.394 + 1.395 + if (entry.owner_b64) { 1.396 + var ownerInput = Cc["@mozilla.org/io/string-input-stream;1"]. 1.397 + createInstance(Ci.nsIStringInputStream); 1.398 + var binaryData = atob(entry.owner_b64); 1.399 + ownerInput.setData(binaryData, binaryData.length); 1.400 + var binaryStream = Cc["@mozilla.org/binaryinputstream;1"]. 1.401 + createInstance(Ci.nsIObjectInputStream); 1.402 + binaryStream.setInputStream(ownerInput); 1.403 + try { // Catch possible deserialization exceptions 1.404 + shEntry.owner = binaryStream.readObject(true); 1.405 + } catch (ex) { debug(ex); } 1.406 + } 1.407 + 1.408 + if (entry.children && shEntry instanceof Ci.nsISHContainer) { 1.409 + for (var i = 0; i < entry.children.length; i++) { 1.410 + //XXXzpao Wallpaper patch for bug 514751 1.411 + if (!entry.children[i].url) 1.412 + continue; 1.413 + 1.414 + // We're getting sessionrestore.js files with a cycle in the 1.415 + // doc-identifier graph, likely due to bug 698656. (That is, we have 1.416 + // an entry where doc identifier A is an ancestor of doc identifier B, 1.417 + // and another entry where doc identifier B is an ancestor of A.) 1.418 + // 1.419 + // If we were to respect these doc identifiers, we'd create a cycle in 1.420 + // the SHEntries themselves, which causes the docshell to loop forever 1.421 + // when it looks for the root SHEntry. 1.422 + // 1.423 + // So as a hack to fix this, we restrict the scope of a doc identifier 1.424 + // to be a node's siblings and cousins, and pass childDocIdents, not 1.425 + // aDocIdents, to _deserializeHistoryEntry. That is, we say that two 1.426 + // SHEntries with the same doc identifier have the same document iff 1.427 + // they have the same parent or their parents have the same document. 1.428 + 1.429 + shEntry.AddChild(this.deserializeEntry(entry.children[i], idMap, 1.430 + childDocIdents), i); 1.431 + } 1.432 + } 1.433 + 1.434 + return shEntry; 1.435 + }, 1.436 + 1.437 +};