browser/components/sessionstore/src/SessionHistory.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
michael@0 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 "use strict";
michael@0 6
michael@0 7 this.EXPORTED_SYMBOLS = ["SessionHistory"];
michael@0 8
michael@0 9 const Cu = Components.utils;
michael@0 10 const Cc = Components.classes;
michael@0 11 const Ci = Components.interfaces;
michael@0 12
michael@0 13 Cu.import("resource://gre/modules/Services.jsm");
michael@0 14 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 15
michael@0 16 XPCOMUtils.defineLazyModuleGetter(this, "Utils",
michael@0 17 "resource:///modules/sessionstore/Utils.jsm");
michael@0 18
michael@0 19 function debug(msg) {
michael@0 20 Services.console.logStringMessage("SessionHistory: " + msg);
michael@0 21 }
michael@0 22
michael@0 23 /**
michael@0 24 * The external API exported by this module.
michael@0 25 */
michael@0 26 this.SessionHistory = Object.freeze({
michael@0 27 isEmpty: function (docShell) {
michael@0 28 return SessionHistoryInternal.isEmpty(docShell);
michael@0 29 },
michael@0 30
michael@0 31 collect: function (docShell) {
michael@0 32 return SessionHistoryInternal.collect(docShell);
michael@0 33 },
michael@0 34
michael@0 35 restore: function (docShell, tabData) {
michael@0 36 SessionHistoryInternal.restore(docShell, tabData);
michael@0 37 }
michael@0 38 });
michael@0 39
michael@0 40 /**
michael@0 41 * The internal API for the SessionHistory module.
michael@0 42 */
michael@0 43 let SessionHistoryInternal = {
michael@0 44 /**
michael@0 45 * Returns whether the given docShell's session history is empty.
michael@0 46 *
michael@0 47 * @param docShell
michael@0 48 * The docShell that owns the session history.
michael@0 49 */
michael@0 50 isEmpty: function (docShell) {
michael@0 51 let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation);
michael@0 52 let history = webNavigation.sessionHistory;
michael@0 53 if (!webNavigation.currentURI) {
michael@0 54 return true;
michael@0 55 }
michael@0 56 let uri = webNavigation.currentURI.spec;
michael@0 57 return uri == "about:blank" && history.count == 0;
michael@0 58 },
michael@0 59
michael@0 60 /**
michael@0 61 * Collects session history data for a given docShell.
michael@0 62 *
michael@0 63 * @param docShell
michael@0 64 * The docShell that owns the session history.
michael@0 65 */
michael@0 66 collect: function (docShell) {
michael@0 67 let data = {entries: []};
michael@0 68 let isPinned = docShell.isAppTab;
michael@0 69 let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation);
michael@0 70 let history = webNavigation.sessionHistory;
michael@0 71
michael@0 72 if (history && history.count > 0) {
michael@0 73 let oldest;
michael@0 74 let maxSerializeBack =
michael@0 75 Services.prefs.getIntPref("browser.sessionstore.max_serialize_back");
michael@0 76 if (maxSerializeBack >= 0) {
michael@0 77 oldest = Math.max(0, history.index - maxSerializeBack);
michael@0 78 } else { // History.getEntryAtIndex(0, ...) is the oldest.
michael@0 79 oldest = 0;
michael@0 80 }
michael@0 81
michael@0 82 let newest;
michael@0 83 let maxSerializeFwd =
michael@0 84 Services.prefs.getIntPref("browser.sessionstore.max_serialize_forward");
michael@0 85 if (maxSerializeFwd >= 0) {
michael@0 86 newest = Math.min(history.count - 1, history.index + maxSerializeFwd);
michael@0 87 } else { // History.getEntryAtIndex(history.count - 1, ...) is the newest.
michael@0 88 newest = history.count - 1;
michael@0 89 }
michael@0 90
michael@0 91 try {
michael@0 92 for (let i = oldest; i <= newest; i++) {
michael@0 93 let shEntry = history.getEntryAtIndex(i, false);
michael@0 94 let entry = this.serializeEntry(shEntry, isPinned);
michael@0 95 data.entries.push(entry);
michael@0 96 }
michael@0 97 } catch (ex) {
michael@0 98 // In some cases, getEntryAtIndex will throw. This seems to be due to
michael@0 99 // history.count being higher than it should be. By doing this in a
michael@0 100 // try-catch, we'll update history to where it breaks, print an error
michael@0 101 // message, and still save sessionstore.js.
michael@0 102 debug("SessionStore failed gathering complete history " +
michael@0 103 "for the focused window/tab. See bug 669196.");
michael@0 104 }
michael@0 105
michael@0 106 // Set the one-based index of the currently active tab,
michael@0 107 // ensuring it isn't out of bounds if an exception was thrown above.
michael@0 108 data.index = Math.min(history.index - oldest + 1, data.entries.length);
michael@0 109 }
michael@0 110
michael@0 111 // If either the session history isn't available yet or doesn't have any
michael@0 112 // valid entries, make sure we at least include the current page.
michael@0 113 if (data.entries.length == 0) {
michael@0 114 let uri = webNavigation.currentURI.spec;
michael@0 115 let body = webNavigation.document.body;
michael@0 116 // We landed here because the history is inaccessible or there are no
michael@0 117 // history entries. In that case we should at least record the docShell's
michael@0 118 // current URL as a single history entry. If the URL is not about:blank
michael@0 119 // or it's a blank tab that was modified (like a custom newtab page),
michael@0 120 // record it. For about:blank we explicitly want an empty array without
michael@0 121 // an 'index' property to denote that there are no history entries.
michael@0 122 if (uri != "about:blank" || (body && body.hasChildNodes())) {
michael@0 123 data.entries.push({ url: uri });
michael@0 124 data.index = 1;
michael@0 125 }
michael@0 126 }
michael@0 127
michael@0 128 return data;
michael@0 129 },
michael@0 130
michael@0 131 /**
michael@0 132 * Determines whether a given session history entry has been added dynamically.
michael@0 133 *
michael@0 134 * @param shEntry
michael@0 135 * The session history entry.
michael@0 136 * @return bool
michael@0 137 */
michael@0 138 isDynamic: function (shEntry) {
michael@0 139 // shEntry.isDynamicallyAdded() is true for dynamically added
michael@0 140 // <iframe> and <frameset>, but also for <html> (the root of the
michael@0 141 // document) so we use shEntry.parent to ensure that we're not looking
michael@0 142 // at the root of the document
michael@0 143 return shEntry.parent && shEntry.isDynamicallyAdded();
michael@0 144 },
michael@0 145
michael@0 146 /**
michael@0 147 * Get an object that is a serialized representation of a History entry.
michael@0 148 *
michael@0 149 * @param shEntry
michael@0 150 * nsISHEntry instance
michael@0 151 * @param isPinned
michael@0 152 * The tab is pinned and should be treated differently for privacy.
michael@0 153 * @return object
michael@0 154 */
michael@0 155 serializeEntry: function (shEntry, isPinned) {
michael@0 156 let entry = { url: shEntry.URI.spec };
michael@0 157
michael@0 158 // Save some bytes and don't include the title property
michael@0 159 // if that's identical to the current entry's URL.
michael@0 160 if (shEntry.title && shEntry.title != entry.url) {
michael@0 161 entry.title = shEntry.title;
michael@0 162 }
michael@0 163 if (shEntry.isSubFrame) {
michael@0 164 entry.subframe = true;
michael@0 165 }
michael@0 166
michael@0 167 let cacheKey = shEntry.cacheKey;
michael@0 168 if (cacheKey && cacheKey instanceof Ci.nsISupportsPRUint32 &&
michael@0 169 cacheKey.data != 0) {
michael@0 170 // XXXbz would be better to have cache keys implement
michael@0 171 // nsISerializable or something.
michael@0 172 entry.cacheKey = cacheKey.data;
michael@0 173 }
michael@0 174 entry.ID = shEntry.ID;
michael@0 175 entry.docshellID = shEntry.docshellID;
michael@0 176
michael@0 177 // We will include the property only if it's truthy to save a couple of
michael@0 178 // bytes when the resulting object is stringified and saved to disk.
michael@0 179 if (shEntry.referrerURI)
michael@0 180 entry.referrer = shEntry.referrerURI.spec;
michael@0 181
michael@0 182 if (shEntry.srcdocData)
michael@0 183 entry.srcdocData = shEntry.srcdocData;
michael@0 184
michael@0 185 if (shEntry.isSrcdocEntry)
michael@0 186 entry.isSrcdocEntry = shEntry.isSrcdocEntry;
michael@0 187
michael@0 188 if (shEntry.baseURI)
michael@0 189 entry.baseURI = shEntry.baseURI.spec;
michael@0 190
michael@0 191 if (shEntry.contentType)
michael@0 192 entry.contentType = shEntry.contentType;
michael@0 193
michael@0 194 let x = {}, y = {};
michael@0 195 shEntry.getScrollPosition(x, y);
michael@0 196 if (x.value != 0 || y.value != 0)
michael@0 197 entry.scroll = x.value + "," + y.value;
michael@0 198
michael@0 199 // Collect owner data for the current history entry.
michael@0 200 try {
michael@0 201 let owner = this.serializeOwner(shEntry);
michael@0 202 if (owner) {
michael@0 203 entry.owner_b64 = owner;
michael@0 204 }
michael@0 205 } catch (ex) {
michael@0 206 // Not catching anything specific here, just possible errors
michael@0 207 // from writeCompoundObject() and the like.
michael@0 208 debug("Failed serializing owner data: " + ex);
michael@0 209 }
michael@0 210
michael@0 211 entry.docIdentifier = shEntry.BFCacheEntry.ID;
michael@0 212
michael@0 213 if (shEntry.stateData != null) {
michael@0 214 entry.structuredCloneState = shEntry.stateData.getDataAsBase64();
michael@0 215 entry.structuredCloneVersion = shEntry.stateData.formatVersion;
michael@0 216 }
michael@0 217
michael@0 218 if (!(shEntry instanceof Ci.nsISHContainer)) {
michael@0 219 return entry;
michael@0 220 }
michael@0 221
michael@0 222 if (shEntry.childCount > 0) {
michael@0 223 let children = [];
michael@0 224 for (let i = 0; i < shEntry.childCount; i++) {
michael@0 225 let child = shEntry.GetChildAt(i);
michael@0 226
michael@0 227 if (child && !this.isDynamic(child)) {
michael@0 228 // Don't try to restore framesets containing wyciwyg URLs.
michael@0 229 // (cf. bug 424689 and bug 450595)
michael@0 230 if (child.URI.schemeIs("wyciwyg")) {
michael@0 231 children.length = 0;
michael@0 232 break;
michael@0 233 }
michael@0 234
michael@0 235 children.push(this.serializeEntry(child, isPinned));
michael@0 236 }
michael@0 237 }
michael@0 238
michael@0 239 if (children.length) {
michael@0 240 entry.children = children;
michael@0 241 }
michael@0 242 }
michael@0 243
michael@0 244 return entry;
michael@0 245 },
michael@0 246
michael@0 247 /**
michael@0 248 * Serialize owner data contained in the given session history entry.
michael@0 249 *
michael@0 250 * @param shEntry
michael@0 251 * The session history entry.
michael@0 252 * @return The base64 encoded owner data.
michael@0 253 */
michael@0 254 serializeOwner: function (shEntry) {
michael@0 255 if (!shEntry.owner) {
michael@0 256 return null;
michael@0 257 }
michael@0 258
michael@0 259 let binaryStream = Cc["@mozilla.org/binaryoutputstream;1"].
michael@0 260 createInstance(Ci.nsIObjectOutputStream);
michael@0 261 let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
michael@0 262 pipe.init(false, false, 0, 0xffffffff, null);
michael@0 263 binaryStream.setOutputStream(pipe.outputStream);
michael@0 264 binaryStream.writeCompoundObject(shEntry.owner, Ci.nsISupports, true);
michael@0 265 binaryStream.close();
michael@0 266
michael@0 267 // Now we want to read the data from the pipe's input end and encode it.
michael@0 268 let scriptableStream = Cc["@mozilla.org/binaryinputstream;1"].
michael@0 269 createInstance(Ci.nsIBinaryInputStream);
michael@0 270 scriptableStream.setInputStream(pipe.inputStream);
michael@0 271 let ownerBytes =
michael@0 272 scriptableStream.readByteArray(scriptableStream.available());
michael@0 273
michael@0 274 // We can stop doing base64 encoding once our serialization into JSON
michael@0 275 // is guaranteed to handle all chars in strings, including embedded
michael@0 276 // nulls.
michael@0 277 return btoa(String.fromCharCode.apply(null, ownerBytes));
michael@0 278 },
michael@0 279
michael@0 280 /**
michael@0 281 * Restores session history data for a given docShell.
michael@0 282 *
michael@0 283 * @param docShell
michael@0 284 * The docShell that owns the session history.
michael@0 285 * @param tabData
michael@0 286 * The tabdata including all history entries.
michael@0 287 */
michael@0 288 restore: function (docShell, tabData) {
michael@0 289 let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation);
michael@0 290 let history = webNavigation.sessionHistory;
michael@0 291
michael@0 292 if (history.count > 0) {
michael@0 293 history.PurgeHistory(history.count);
michael@0 294 }
michael@0 295 history.QueryInterface(Ci.nsISHistoryInternal);
michael@0 296
michael@0 297 let idMap = { used: {} };
michael@0 298 let docIdentMap = {};
michael@0 299 for (let i = 0; i < tabData.entries.length; i++) {
michael@0 300 //XXXzpao Wallpaper patch for bug 514751
michael@0 301 if (!tabData.entries[i].url)
michael@0 302 continue;
michael@0 303 history.addEntry(this.deserializeEntry(tabData.entries[i],
michael@0 304 idMap, docIdentMap), true);
michael@0 305 }
michael@0 306 },
michael@0 307
michael@0 308 /**
michael@0 309 * Expands serialized history data into a session-history-entry instance.
michael@0 310 *
michael@0 311 * @param entry
michael@0 312 * Object containing serialized history data for a URL
michael@0 313 * @param idMap
michael@0 314 * Hash for ensuring unique frame IDs
michael@0 315 * @param docIdentMap
michael@0 316 * Hash to ensure reuse of BFCache entries
michael@0 317 * @returns nsISHEntry
michael@0 318 */
michael@0 319 deserializeEntry: function (entry, idMap, docIdentMap) {
michael@0 320
michael@0 321 var shEntry = Cc["@mozilla.org/browser/session-history-entry;1"].
michael@0 322 createInstance(Ci.nsISHEntry);
michael@0 323
michael@0 324 shEntry.setURI(Utils.makeURI(entry.url));
michael@0 325 shEntry.setTitle(entry.title || entry.url);
michael@0 326 if (entry.subframe)
michael@0 327 shEntry.setIsSubFrame(entry.subframe || false);
michael@0 328 shEntry.loadType = Ci.nsIDocShellLoadInfo.loadHistory;
michael@0 329 if (entry.contentType)
michael@0 330 shEntry.contentType = entry.contentType;
michael@0 331 if (entry.referrer)
michael@0 332 shEntry.referrerURI = Utils.makeURI(entry.referrer);
michael@0 333 if (entry.isSrcdocEntry)
michael@0 334 shEntry.srcdocData = entry.srcdocData;
michael@0 335 if (entry.baseURI)
michael@0 336 shEntry.baseURI = Utils.makeURI(entry.baseURI);
michael@0 337
michael@0 338 if (entry.cacheKey) {
michael@0 339 var cacheKey = Cc["@mozilla.org/supports-PRUint32;1"].
michael@0 340 createInstance(Ci.nsISupportsPRUint32);
michael@0 341 cacheKey.data = entry.cacheKey;
michael@0 342 shEntry.cacheKey = cacheKey;
michael@0 343 }
michael@0 344
michael@0 345 if (entry.ID) {
michael@0 346 // get a new unique ID for this frame (since the one from the last
michael@0 347 // start might already be in use)
michael@0 348 var id = idMap[entry.ID] || 0;
michael@0 349 if (!id) {
michael@0 350 for (id = Date.now(); id in idMap.used; id++);
michael@0 351 idMap[entry.ID] = id;
michael@0 352 idMap.used[id] = true;
michael@0 353 }
michael@0 354 shEntry.ID = id;
michael@0 355 }
michael@0 356
michael@0 357 if (entry.docshellID)
michael@0 358 shEntry.docshellID = entry.docshellID;
michael@0 359
michael@0 360 if (entry.structuredCloneState && entry.structuredCloneVersion) {
michael@0 361 shEntry.stateData =
michael@0 362 Cc["@mozilla.org/docshell/structured-clone-container;1"].
michael@0 363 createInstance(Ci.nsIStructuredCloneContainer);
michael@0 364
michael@0 365 shEntry.stateData.initFromBase64(entry.structuredCloneState,
michael@0 366 entry.structuredCloneVersion);
michael@0 367 }
michael@0 368
michael@0 369 if (entry.scroll) {
michael@0 370 var scrollPos = (entry.scroll || "0,0").split(",");
michael@0 371 scrollPos = [parseInt(scrollPos[0]) || 0, parseInt(scrollPos[1]) || 0];
michael@0 372 shEntry.setScrollPosition(scrollPos[0], scrollPos[1]);
michael@0 373 }
michael@0 374
michael@0 375 let childDocIdents = {};
michael@0 376 if (entry.docIdentifier) {
michael@0 377 // If we have a serialized document identifier, try to find an SHEntry
michael@0 378 // which matches that doc identifier and adopt that SHEntry's
michael@0 379 // BFCacheEntry. If we don't find a match, insert shEntry as the match
michael@0 380 // for the document identifier.
michael@0 381 let matchingEntry = docIdentMap[entry.docIdentifier];
michael@0 382 if (!matchingEntry) {
michael@0 383 matchingEntry = {shEntry: shEntry, childDocIdents: childDocIdents};
michael@0 384 docIdentMap[entry.docIdentifier] = matchingEntry;
michael@0 385 }
michael@0 386 else {
michael@0 387 shEntry.adoptBFCacheEntry(matchingEntry.shEntry);
michael@0 388 childDocIdents = matchingEntry.childDocIdents;
michael@0 389 }
michael@0 390 }
michael@0 391
michael@0 392 if (entry.owner_b64) {
michael@0 393 var ownerInput = Cc["@mozilla.org/io/string-input-stream;1"].
michael@0 394 createInstance(Ci.nsIStringInputStream);
michael@0 395 var binaryData = atob(entry.owner_b64);
michael@0 396 ownerInput.setData(binaryData, binaryData.length);
michael@0 397 var binaryStream = Cc["@mozilla.org/binaryinputstream;1"].
michael@0 398 createInstance(Ci.nsIObjectInputStream);
michael@0 399 binaryStream.setInputStream(ownerInput);
michael@0 400 try { // Catch possible deserialization exceptions
michael@0 401 shEntry.owner = binaryStream.readObject(true);
michael@0 402 } catch (ex) { debug(ex); }
michael@0 403 }
michael@0 404
michael@0 405 if (entry.children && shEntry instanceof Ci.nsISHContainer) {
michael@0 406 for (var i = 0; i < entry.children.length; i++) {
michael@0 407 //XXXzpao Wallpaper patch for bug 514751
michael@0 408 if (!entry.children[i].url)
michael@0 409 continue;
michael@0 410
michael@0 411 // We're getting sessionrestore.js files with a cycle in the
michael@0 412 // doc-identifier graph, likely due to bug 698656. (That is, we have
michael@0 413 // an entry where doc identifier A is an ancestor of doc identifier B,
michael@0 414 // and another entry where doc identifier B is an ancestor of A.)
michael@0 415 //
michael@0 416 // If we were to respect these doc identifiers, we'd create a cycle in
michael@0 417 // the SHEntries themselves, which causes the docshell to loop forever
michael@0 418 // when it looks for the root SHEntry.
michael@0 419 //
michael@0 420 // So as a hack to fix this, we restrict the scope of a doc identifier
michael@0 421 // to be a node's siblings and cousins, and pass childDocIdents, not
michael@0 422 // aDocIdents, to _deserializeHistoryEntry. That is, we say that two
michael@0 423 // SHEntries with the same doc identifier have the same document iff
michael@0 424 // they have the same parent or their parents have the same document.
michael@0 425
michael@0 426 shEntry.AddChild(this.deserializeEntry(entry.children[i], idMap,
michael@0 427 childDocIdents), i);
michael@0 428 }
michael@0 429 }
michael@0 430
michael@0 431 return shEntry;
michael@0 432 },
michael@0 433
michael@0 434 };

mercurial