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.

     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 };

mercurial