|
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/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 this.EXPORTED_SYMBOLS = ["SessionHistory"]; |
|
8 |
|
9 const Cu = Components.utils; |
|
10 const Cc = Components.classes; |
|
11 const Ci = Components.interfaces; |
|
12 |
|
13 Cu.import("resource://gre/modules/Services.jsm"); |
|
14 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
15 |
|
16 XPCOMUtils.defineLazyModuleGetter(this, "Utils", |
|
17 "resource:///modules/sessionstore/Utils.jsm"); |
|
18 |
|
19 function debug(msg) { |
|
20 Services.console.logStringMessage("SessionHistory: " + msg); |
|
21 } |
|
22 |
|
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 }, |
|
30 |
|
31 collect: function (docShell) { |
|
32 return SessionHistoryInternal.collect(docShell); |
|
33 }, |
|
34 |
|
35 restore: function (docShell, tabData) { |
|
36 SessionHistoryInternal.restore(docShell, tabData); |
|
37 } |
|
38 }); |
|
39 |
|
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 }, |
|
59 |
|
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; |
|
71 |
|
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 } |
|
81 |
|
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 } |
|
90 |
|
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 } |
|
105 |
|
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 } |
|
110 |
|
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 } |
|
127 |
|
128 return data; |
|
129 }, |
|
130 |
|
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 }, |
|
145 |
|
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 }; |
|
157 |
|
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 } |
|
166 |
|
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; |
|
176 |
|
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; |
|
181 |
|
182 if (shEntry.srcdocData) |
|
183 entry.srcdocData = shEntry.srcdocData; |
|
184 |
|
185 if (shEntry.isSrcdocEntry) |
|
186 entry.isSrcdocEntry = shEntry.isSrcdocEntry; |
|
187 |
|
188 if (shEntry.baseURI) |
|
189 entry.baseURI = shEntry.baseURI.spec; |
|
190 |
|
191 if (shEntry.contentType) |
|
192 entry.contentType = shEntry.contentType; |
|
193 |
|
194 let x = {}, y = {}; |
|
195 shEntry.getScrollPosition(x, y); |
|
196 if (x.value != 0 || y.value != 0) |
|
197 entry.scroll = x.value + "," + y.value; |
|
198 |
|
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 } |
|
210 |
|
211 entry.docIdentifier = shEntry.BFCacheEntry.ID; |
|
212 |
|
213 if (shEntry.stateData != null) { |
|
214 entry.structuredCloneState = shEntry.stateData.getDataAsBase64(); |
|
215 entry.structuredCloneVersion = shEntry.stateData.formatVersion; |
|
216 } |
|
217 |
|
218 if (!(shEntry instanceof Ci.nsISHContainer)) { |
|
219 return entry; |
|
220 } |
|
221 |
|
222 if (shEntry.childCount > 0) { |
|
223 let children = []; |
|
224 for (let i = 0; i < shEntry.childCount; i++) { |
|
225 let child = shEntry.GetChildAt(i); |
|
226 |
|
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 } |
|
234 |
|
235 children.push(this.serializeEntry(child, isPinned)); |
|
236 } |
|
237 } |
|
238 |
|
239 if (children.length) { |
|
240 entry.children = children; |
|
241 } |
|
242 } |
|
243 |
|
244 return entry; |
|
245 }, |
|
246 |
|
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 } |
|
258 |
|
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(); |
|
266 |
|
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()); |
|
273 |
|
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 }, |
|
279 |
|
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; |
|
291 |
|
292 if (history.count > 0) { |
|
293 history.PurgeHistory(history.count); |
|
294 } |
|
295 history.QueryInterface(Ci.nsISHistoryInternal); |
|
296 |
|
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 }, |
|
307 |
|
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) { |
|
320 |
|
321 var shEntry = Cc["@mozilla.org/browser/session-history-entry;1"]. |
|
322 createInstance(Ci.nsISHEntry); |
|
323 |
|
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); |
|
337 |
|
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 } |
|
344 |
|
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 } |
|
356 |
|
357 if (entry.docshellID) |
|
358 shEntry.docshellID = entry.docshellID; |
|
359 |
|
360 if (entry.structuredCloneState && entry.structuredCloneVersion) { |
|
361 shEntry.stateData = |
|
362 Cc["@mozilla.org/docshell/structured-clone-container;1"]. |
|
363 createInstance(Ci.nsIStructuredCloneContainer); |
|
364 |
|
365 shEntry.stateData.initFromBase64(entry.structuredCloneState, |
|
366 entry.structuredCloneVersion); |
|
367 } |
|
368 |
|
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 } |
|
374 |
|
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 } |
|
391 |
|
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 } |
|
404 |
|
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; |
|
410 |
|
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. |
|
425 |
|
426 shEntry.AddChild(this.deserializeEntry(entry.children[i], idMap, |
|
427 childDocIdents), i); |
|
428 } |
|
429 } |
|
430 |
|
431 return shEntry; |
|
432 }, |
|
433 |
|
434 }; |