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