Attachment #662071: Part 1 - Create SessionHistory.jsm for bug #759782

View | Details | Raw Unified | Return to bug 759782
Collapse All | Expand All

(-)a/browser/components/sessionstore/src/Makefile.in (+1 lines)
Line     Link Here 
 Lines 16-31   EXTRA_COMPONENTS = \ Link Here 
16
	nsSessionStartup.js \
16
	nsSessionStartup.js \
17
  $(NULL)
17
  $(NULL)
18
18
19
JS_MODULES_PATH := $(FINAL_TARGET)/modules/sessionstore
19
JS_MODULES_PATH := $(FINAL_TARGET)/modules/sessionstore
20
20
21
EXTRA_JS_MODULES := \
21
EXTRA_JS_MODULES := \
22
  DocumentUtils.jsm \
22
  DocumentUtils.jsm \
23
  SessionStorage.jsm \
23
  SessionStorage.jsm \
24
  SessionHistory.jsm \
24
  XPathGenerator.jsm \
25
  XPathGenerator.jsm \
25
  $(NULL)
26
  $(NULL)
26
27
27
EXTRA_PP_JS_MODULES := \
28
EXTRA_PP_JS_MODULES := \
28
	SessionStore.jsm \
29
	SessionStore.jsm \
29
	$(NULL)
30
	$(NULL)
30
31
31
include $(topsrcdir)/config/rules.mk
32
include $(topsrcdir)/config/rules.mk
(-)a/browser/components/sessionstore/src/SessionHistory.jsm (+391 lines)
Line     Link Here 
Line 0    Link Here 
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
let EXPORTED_SYMBOLS = ["SessionHistory"];
6
7
const Cu = Components.utils;
8
const Cc = Components.classes;
9
const Ci = Components.interfaces;
10
11
Cu.import("resource://gre/modules/Services.jsm");
12
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
13
14
XPCOMUtils.defineLazyModuleGetter(this, "SessionStore",
15
  "resource:///modules/sessionstore/SessionStore.jsm");
16
17
const PREF_MAX_POSTDATA_LENGTH = "browser.sessionstore.postdata";
18
19
function debug(aMsg) {
20
  aMsg = ("SessionHistory: " + aMsg).replace(/\S{80}/g, "$&\n");
21
  Services.console.logStringMessage(aMsg);
22
}
23
24
let SessionHistory = {
25
  getEntries: function sshi_getEntries(aHistory) {
26
    let entries = [];
27
28
    for (let i = 0; i < aHistory.count; i++) {
29
      try {
30
        // In some cases, getEntryAtIndex will throw. This seems to be due to
31
        // history.count being higher than it should be. By doing this in a
32
        // try-catch, we'll update history to where it breaks, assert for
33
        // non-release builds, and still save sessionstore.js. We'll track if
34
        // we've shown the assert for this tab so we only show it once.
35
        // cf. bug 669196.
36
        entries.push(aHistory.getEntryAtIndex(i, false));
37
      } catch (e) {
38
        let error = new Error("Can't serialize a broken sessionHistory (bug 669196)");
39
        error.name = "BrokenSessionHistoryError";
40
        throw error;
41
      }
42
    }
43
44
    return entries;
45
  },
46
47
  serialize: function sshi_serialize(aDocShell, aFullData) {
48
    let entries = [];
49
    let isPinned = aDocShell.isAppTab;
50
    let shistory = aDocShell.sessionHistory;
51
52
    for (let shentry of this.getEntries(shistory)) {
53
      let entry = SessionHistoryInternal.serializeEntry(shentry, aFullData, isPinned);
54
      entries.push(entry);
55
    }
56
57
    return entries;
58
  },
59
60
  deserialize: function sshi_deserialize(aDocShell, aEntries) {
61
    let shistory = aDocShell.sessionHistory.QueryInterface(Ci.nsISHistoryInternal);
62
    let idMap = { used: {} };
63
    let docIDMap = {};
64
65
    for (let i = 0; i < aEntries.length; i++) {
66
      let entry = aEntries[i];
67
      if (entry.url) { //XXXzpao Wallpaper patch for bug 514751
68
        let shentry = SessionHistoryInternal.deserializeEntry(entry, idMap, docIDMap);
69
        shistory.addEntry(shentry, true);
70
      }
71
    }
72
  }
73
};
74
75
Object.freeze(SessionHistory);
76
77
let SessionHistoryInternal = {
78
  get postDataMaxLength() {
79
    return Services.prefs.getIntPref(PREF_MAX_POSTDATA_LENGTH);
80
  },
81
82
  /**
83
   * Get an object that is a serialized representation of a History entry
84
   * Used for data storage
85
   * @param aEntry
86
   *        nsISHEntry instance
87
   * @param aFullData
88
   *        always return privacy sensitive data (use with care)
89
   * @param aIsPinned
90
   *        the tab is pinned and should be treated differently for privacy
91
   * @returns object
92
   */
93
  serializeEntry: function ssihi_serializeEntry(aEntry, aFullData, aIsPinned) {
94
    let uri = aEntry.URI;
95
    let entry = {url: uri.spec};
96
97
    try {
98
      // Throwing is expensive, we know that about: pages will throw.
99
      if (entry.url.indexOf("about:") != 0) {
100
        entry.host = uri.host;
101
        entry.scheme = uri.scheme;
102
      }
103
    } catch (ex) {
104
      // We'll just not store host and scheme for this entry.
105
    }
106
107
    if (aEntry.title && aEntry.title != entry.url) {
108
      entry.title = aEntry.title;
109
    }
110
    if (aEntry.isSubFrame) {
111
      entry.subframe = true;
112
    }
113
    if (!(aEntry instanceof Ci.nsISHEntry)) {
114
      return entry;
115
    }
116
117
    let cacheKey = aEntry.cacheKey;
118
    if (cacheKey && cacheKey instanceof Ci.nsISupportsPRUint32 &&
119
        cacheKey.data != 0) {
120
      // XXXbz would be better to have cache keys implement
121
      // nsISerializable or something.
122
      entry.cacheKey = cacheKey.data;
123
    }
124
    entry.ID = aEntry.ID;
125
    entry.docshellID = aEntry.docshellID;
126
127
    if (aEntry.referrerURI)
128
      entry.referrer = aEntry.referrerURI.spec;
129
130
    if (aEntry.contentType)
131
      entry.contentType = aEntry.contentType;
132
133
    let x = {}, y = {};
134
    aEntry.getScrollPosition(x, y);
135
    if (x.value != 0 || y.value != 0)
136
      entry.scroll = x.value + "," + y.value;
137
138
    this._serializePostData(entry, aEntry, aFullData);
139
    this._serializeOwner(entry, aEntry);
140
141
    entry.docIdentifier = aEntry.BFCacheEntry.ID;
142
143
    if (aEntry.stateData != null) {
144
      entry.structuredCloneState = aEntry.stateData.getDataAsBase64();
145
      entry.structuredCloneVersion = aEntry.stateData.formatVersion;
146
    }
147
148
    if (!(aEntry instanceof Ci.nsISHContainer)) {
149
      return entry;
150
    }
151
152
    if (aEntry.childCount > 0) {
153
      let children = [];
154
      for (let i = 0; i < aEntry.childCount; i++) {
155
        let child = aEntry.GetChildAt(i);
156
157
        if (child) {
158
          // don't try to restore framesets containing wyciwyg URLs (cf. bug 424689 and bug 450595)
159
          if (child.URI.schemeIs("wyciwyg")) {
160
            children = [];
161
            break;
162
          }
163
164
          children.push(this.serializeEntry(child, aFullData, aIsPinned));
165
        }
166
      }
167
168
      if (children.length)
169
        entry.children = children;
170
    }
171
172
    return entry;
173
  },
174
175
  /**
176
   * expands serialized history data into a session-history-entry instance
177
   * @param aEntry
178
   *        Object containing serialized history data for a URL
179
   * @param aIdMap
180
   *        Hash for ensuring unique frame IDs
181
   * @returns nsISHEntry
182
   */
183
  deserializeEntry: function ssihi_deserializeEntry(aEntry, aIdMap, aDocIdentMap) {
184
    let shEntry = Cc["@mozilla.org/browser/session-history-entry;1"]
185
                    .createInstance(Ci.nsISHEntry);
186
187
    shEntry.setURI(Services.io.newURI(aEntry.url, null, null));
188
    shEntry.setTitle(aEntry.title || aEntry.url);
189
    if (aEntry.subframe)
190
      shEntry.setIsSubFrame(aEntry.subframe || false);
191
    shEntry.loadType = Ci.nsIDocShellLoadInfo.loadHistory;
192
    if (aEntry.contentType)
193
      shEntry.contentType = aEntry.contentType;
194
    if (aEntry.referrer)
195
      shEntry.referrerURI = Services.io.newURI(aEntry.referrer, null, null)
196
197
    if (aEntry.cacheKey) {
198
      let cacheKey = Cc["@mozilla.org/supports-PRUint32;1"].
199
                     createInstance(Ci.nsISupportsPRUint32);
200
      cacheKey.data = aEntry.cacheKey;
201
      shEntry.cacheKey = cacheKey;
202
    }
203
204
    if (aEntry.ID) {
205
      // get a new unique ID for this frame (since the one from the last
206
      // start might already be in use)
207
      let id = aIdMap[aEntry.ID] || 0;
208
      if (!id) {
209
        for (id = Date.now(); id in aIdMap.used; id++);
210
        aIdMap[aEntry.ID] = id;
211
        aIdMap.used[id] = true;
212
      }
213
      shEntry.ID = id;
214
    }
215
216
    if (aEntry.docshellID)
217
      shEntry.docshellID = aEntry.docshellID;
218
219
    if (aEntry.structuredCloneState && aEntry.structuredCloneVersion) {
220
      shEntry.stateData =
221
        Cc["@mozilla.org/docshell/structured-clone-container;1"].
222
        createInstance(Ci.nsIStructuredCloneContainer);
223
224
      shEntry.stateData.initFromBase64(aEntry.structuredCloneState,
225
                                       aEntry.structuredCloneVersion);
226
    }
227
228
    if (aEntry.scroll) {
229
      let scrollPos = (aEntry.scroll || "0,0").split(",");
230
      scrollPos = [parseInt(scrollPos[0]) || 0, parseInt(scrollPos[1]) || 0];
231
      shEntry.setScrollPosition(scrollPos[0], scrollPos[1]);
232
    }
233
234
    if (aEntry.postdata_b64)
235
      PostData.deserialize(shEntry, atob(aEntry.postdata_b64))
236
237
    let childDocIdents = {};
238
    if (aEntry.docIdentifier) {
239
      // If we have a serialized document identifier, try to find an SHEntry
240
      // which matches that doc identifier and adopt that SHEntry's
241
      // BFCacheEntry.  If we don't find a match, insert shEntry as the match
242
      // for the document identifier.
243
      let matchingEntry = aDocIdentMap[aEntry.docIdentifier];
244
      if (!matchingEntry) {
245
        matchingEntry = {shEntry: shEntry, childDocIdents: childDocIdents};
246
        aDocIdentMap[aEntry.docIdentifier] = matchingEntry;
247
      }
248
      else {
249
        shEntry.adoptBFCacheEntry(matchingEntry.shEntry);
250
        childDocIdents = matchingEntry.childDocIdents;
251
      }
252
    }
253
254
    if (aEntry.owner_b64)
255
      Owner.deserialize(shEntry, atob(aEntry.owner_b64));
256
257
    if (aEntry.children && shEntry instanceof Ci.nsISHContainer) {
258
      for (let i = 0; i < aEntry.children.length; i++) {
259
        //XXXzpao Wallpaper patch for bug 514751
260
        if (!aEntry.children[i].url)
261
          continue;
262
263
        // We're getting sessionrestore.js files with a cycle in the
264
        // doc-identifier graph, likely due to bug 698656.  (That is, we have
265
        // an entry where doc identifier A is an ancestor of doc identifier B,
266
        // and another entry where doc identifier B is an ancestor of A.)
267
        //
268
        // If we were to respect these doc identifiers, we'd create a cycle in
269
        // the SHEntries themselves, which causes the docshell to loop forever
270
        // when it looks for the root SHEntry.
271
        //
272
        // So as a hack to fix this, we restrict the scope of a doc identifier
273
        // to be a node's siblings and cousins, and pass childDocIdents, not
274
        // aDocIdents, to _deserializeHistoryEntry.  That is, we say that two
275
        // SHEntries with the same doc identifier have the same document iff
276
        // they have the same parent or their parents have the same document.
277
278
        let child = this.deserializeEntry(aEntry.children[i], aIdMap, childDocIdents);
279
        shEntry.AddChild(child, i);
280
      }
281
    }
282
283
    return shEntry;
284
  },
285
286
  _serializePostData: function ssihi_serializePostData(aStore, aEntry, aFullData) {
287
    if (!aEntry.postData)
288
      return;
289
290
    let isHttps = aEntry.URI.schemeIs("https");
291
    let maxLength = aFullData ? -1 : this.postDataMaxLength;
292
    if (maxLength && SessionStore.checkPrivacyLevel(isHttps, aIsPinned)) {
293
      let postData = PostData.serialize(aEntry, maxLength);
294
      if (postData) {
295
        // We can stop doing base64 encoding once our serialization into JSON
296
        // is guaranteed to handle all chars in strings, including embedded
297
        // nulls.
298
        aStore.postdata_b64 = btoa(postData);
299
      }
300
    }
301
  },
302
303
  _serializeOwner: function ssihi_serializeOwner(aStore, aEntry) {
304
    if (!aEntry.owner)
305
      return;
306
307
    let owner = Owner.serialize(aEntry);
308
    if (owner) {
309
      // We can stop doing base64 encoding once our serialization into JSON
310
      // is guaranteed to handle all chars in strings, including embedded
311
      // nulls.
312
      aStore.owner_b64 = btoa(owner);
313
    }
314
  }
315
};
316
317
let PostData = {
318
  REGEX_CONTENT_LENGTH: /^(Content-.*\r\n)+(\r\n)*/,
319
320
  serialize: function PostData_serialize(aEntry, aMaxLength) {
321
    try {
322
      let istream = aEntry.postData.QueryInterface(Ci.nsISeekableStream)
323
      istream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
324
325
      let bistream = Cc["@mozilla.org/binaryinputstream;1"]
326
                       .createInstance(Ci.nsIBinaryInputStream);
327
      bistream.setInputStream(aEntry.postData);
328
329
      let postBytes = bistream.readByteArray(stream.available());
330
      let postdata = String.fromCharCode.apply(null, postBytes);
331
332
      let re = this.REGEX_CONTENT_LENGTH;
333
      if (aMaxLength == -1 || postdata.replace(re, "").length <= aMaxLength)
334
        return postdata;
335
    } catch (ex) {
336
      // POSTDATA is tricky - especially since some extensions don't get it right
337
      debug(ex);
338
    }
339
  },
340
341
  deserialize: function PostData_deserialize(aEntry, aPostData) {
342
    let stream = Cc["@mozilla.org/io/string-input-stream;1"]
343
                   .createInstance(Ci.nsIStringInputStream);
344
    stream.setData(postdata, postdata.length);
345
    aEntry.postData = stream;
346
  }
347
};
348
349
let Owner = {
350
  serialize: function Owner_serialize(aEntry) {
351
    try {
352
      let bostream = Cc["@mozilla.org/binaryoutputstream;1"]
353
                       .createInstance(Ci.nsIObjectOutputStream);
354
355
      let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
356
      pipe.init(false, false, 0, 0xffffffff, null);
357
      bostream.setOutputStream(pipe.outputStream);
358
      bostream.writeCompoundObject(aEntry.owner, Ci.nsISupports, true);
359
      bostream.close();
360
361
      // Now we want to read the data from the pipe's input end and encode it.
362
      let bistream = Cc["@mozilla.org/binaryinputstream;1"]
363
                       .createInstance(Ci.nsIBinaryInputStream);
364
      bistream.setInputStream(pipe.inputStream);
365
366
      let ownerBytes = bistream.readByteArray(bistream.available());
367
      return String.fromCharCode.apply(null, ownerBytes);
368
    } catch (ex) {
369
      // Not catching anything specific here, just possible errors
370
      // from writeCompoundObject and the like.
371
      debug(ex);
372
    }
373
  },
374
375
  deserialize: function Owner_deserialize(aEntry, aData) {
376
    let sistream = Cc["@mozilla.org/io/string-input-stream;1"]
377
                     .createInstance(Ci.nsIStringInputStream);
378
    sistream.setData(aData, aData.length);
379
380
    let bistream = Cc["@mozilla.org/binaryinputstream;1"]
381
                     .createInstance(Ci.nsIObjectInputStream);
382
    bistream.setInputStream(sistream);
383
384
    try {
385
      aEntry.owner = bistream.readObject(true);
386
    } catch (ex) {
387
      // Catch possible deserialization exceptions
388
      debug(ex);
389
    }
390
  }
391
};

Return to bug 759782