merge m-c to fx-team
authorTim Taubert <tim.taubert@gmx.de>
Wed, 09 Nov 2011 10:53:12 +0100
changeset 80068 84294675d68a6859ab4c1a9c687b35ab442b204a
parent 80059 6d42793c480712addea2754bbad42b20f8f1b281 (current diff)
parent 80067 3b2109405ac775b0bd82d5c0136b56ffab7c6282 (diff)
child 80069 4fb61ebbf8ff93868a89c9b85ab0419da511e611
child 80311 7e1f046b173b6e9fea3924613cb58e59967d8dfe
push id3273
push usermak77@bonardo.net
push dateWed, 09 Nov 2011 13:12:26 +0000
treeherdermozilla-inbound@cc41220dbe39 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone11.0a1
first release with
nightly win64
84294675d68a / 11.0a1 / 20111109031111 / files
nightly linux32
nightly linux64
nightly mac
nightly win32
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly win64
merge m-c to fx-team
browser/components/sessionstore/test/browser/browser_476161.js
browser/components/sessionstore/test/browser/browser_476161_sample.html
--- a/browser/components/sessionstore/src/nsSessionStore.js
+++ b/browser/components/sessionstore/src/nsSessionStore.js
@@ -112,21 +112,16 @@ eg: browser.docShell["allow" + aCapabili
 XXX keep these in sync with all the attributes starting
     with "allow" in /docshell/base/nsIDocShell.idl
 */
 const CAPABILITIES = [
   "Subframes", "Plugins", "Javascript", "MetaRedirects", "Images",
   "DNSPrefetch", "Auth", "WindowControl"
 ];
 
-// These keys are for internal use only - they shouldn't be part of the JSON
-// that gets saved to disk nor part of the strings returned by the API.
-const INTERNAL_KEYS = ["_tabStillLoading", "_hosts", "_formDataSaved",
-                       "_shouldRestore", "_host", "_scheme"];
-
 // These are tab events that we listen to.
 const TAB_EVENTS = ["TabOpen", "TabClose", "TabSelect", "TabShow", "TabHide",
                     "TabPinned", "TabUnpinned"];
 
 #ifndef XP_WIN
 #define BROKEN_WM_Z_ORDER
 #endif
 
@@ -211,16 +206,19 @@ SessionStoreService.prototype = {
 
   // time in milliseconds when the session was started (saved across sessions),
   // defaults to now if no session was restored or timestamp doesn't exist
   _sessionStartTime: Date.now(),
 
   // states for all currently opened windows
   _windows: {},
 
+  // internal states for all open windows (data we need to associate, but not write to disk)
+  _internalWindows: {},
+
   // states for all recently closed windows
   _closedWindows: [],
 
   // not-"dirty" windows usually don't need to have their data updated
   _dirtyWindows: {},
 
   // collection of session states yet to be restored
   _statesToRestore: {},
@@ -374,16 +372,21 @@ SessionStoreService.prototype = {
 
           // make sure that at least the first window doesn't have anything hidden
           delete this._initialState.windows[0].hidden;
           // Since nothing is hidden in the first window, it cannot be a popup
           delete this._initialState.windows[0].isPopup;
           // We don't want to minimize and then open a window at startup.
           if (this._initialState.windows[0].sizemode == "minimized")
             this._initialState.windows[0].sizemode = "normal";
+          // clear any lastSessionWindowID attributes since those don't matter
+          // during normal restore
+          this._initialState.windows.forEach(function(aWindow) {
+            delete aWindow.__lastSessionWindowID;
+          });
         }
       }
       catch (ex) { debug("The session file is invalid: " + ex); }
     }
 
     if (this._resume_from_crash) {
       // create a backup if the session data file exists
       try {
@@ -545,27 +548,33 @@ SessionStoreService.prototype = {
       // quit-application notification so the browser is about to exit.
       if (this._loadState == STATE_QUITTING)
         return;
       this._lastSessionState = null;
       let openWindows = {};
       this._forEachBrowserWindow(function(aWindow) {
         Array.forEach(aWindow.gBrowser.tabs, function(aTab) {
           delete aTab.linkedBrowser.__SS_data;
+          delete aTab.linkedBrowser.__SS_tabStillLoading;
+          delete aTab.linkedBrowser.__SS_formDataSaved;
+          delete aTab.linkedBrowser.__SS_hostSchemeData;
           if (aTab.linkedBrowser.__SS_restoreState)
             this._resetTabRestoringState(aTab);
         });
         openWindows[aWindow.__SSi] = true;
       });
       // also clear all data about closed tabs and windows
       for (let ix in this._windows) {
-        if (ix in openWindows)
+        if (ix in openWindows) {
           this._windows[ix]._closedTabs = [];
-        else
+        }
+        else {
           delete this._windows[ix];
+          delete this._internalWindows[ix];
+        }
       }
       // also clear all data about closed windows
       this._closedWindows = [];
       // give the tabbrowsers a chance to clear their histories first
       var win = this._getMostRecentBrowserWindow();
       if (win)
         win.setTimeout(function() { _this.saveState(true); }, 0);
       else if (this._loadState == STATE_RUNNING)
@@ -792,16 +801,20 @@ SessionStoreService.prototype = {
         this._loadState == STATE_QUITTING)
       return;
 
     // assign it a unique identifier (timestamp)
     aWindow.__SSi = "window" + Date.now();
 
     // and create its data object
     this._windows[aWindow.__SSi] = { tabs: [], selected: 0, _closedTabs: [], busy: false };
+
+    // and create its internal data object
+    this._internalWindows[aWindow.__SSi] = { hosts: {} }
+
     if (!this._isWindowLoaded(aWindow))
       this._windows[aWindow.__SSi]._restoring = true;
     if (!aWindow.toolbar.visible)
       this._windows[aWindow.__SSi].isPopup = true;
     
     // perform additional initialization when the first window is loading
     if (this._loadState == STATE_STOPPED) {
       this._loadState = STATE_RUNNING;
@@ -963,17 +976,19 @@ SessionStoreService.prototype = {
     if (this._loadState == STATE_RUNNING) { // window not closed during a regular shut-down 
       // update all window data for a last time
       this._collectWindowData(aWindow);
       
       if (isFullyLoaded) {
         winData.title = aWindow.content.document.title || tabbrowser.selectedTab.label;
         winData.title = this._replaceLoadingTitle(winData.title, tabbrowser,
                                                   tabbrowser.selectedTab);
-        this._updateCookies([winData]);
+        let windows = {};
+        windows[aWindow.__SSi] = winData;
+        this._updateCookies(windows);
       }
 
 #ifndef XP_MACOSX
       // Until we decide otherwise elsewhere, this window is part of a series
       // of closing windows to quit.
       winData._shouldRestore = true;
 #endif
 
@@ -984,16 +999,17 @@ SessionStoreService.prototype = {
         delete winData.busy;
 
         this._closedWindows.unshift(winData);
         this._capClosedWindows();
       }
       
       // clear this window from the list
       delete this._windows[aWindow.__SSi];
+      delete this._internalWindows[aWindow.__SSi];
       
       // save the state without this window to disk
       this.saveStateDelayed();
     }
     
     for (let i = 0; i < tabbrowser.tabs.length; i++) {
       this.onTabRemove(aWindow, tabbrowser.tabs[i], true);
     }
@@ -1041,16 +1057,19 @@ SessionStoreService.prototype = {
     let browser = aTab.linkedBrowser;
     browser.removeEventListener("load", this, true);
     browser.removeEventListener("pageshow", this, true);
     browser.removeEventListener("change", this, true);
     browser.removeEventListener("input", this, true);
     browser.removeEventListener("DOMAutoComplete", this, true);
 
     delete browser.__SS_data;
+    delete browser.__SS_tabStillLoading;
+    delete browser.__SS_formDataSaved;
+    delete browser.__SS_hostSchemeData;
 
     // If this tab was in the middle of restoring or still needs to be restored,
     // we need to reset that state. If the tab was restoring, we will attempt to
     // restore the next tab.
     let previousState;
     if (previousState = browser.__SS_restoreState) {
       this._resetTabRestoringState(aTab);
       if (previousState == TAB_STATE_RESTORING)
@@ -1120,33 +1139,35 @@ SessionStoreService.prototype = {
     // overwriting tabs). We want to return early if the tab hasn't been restored yet.
     if ((aEvent.type != "load" && !aEvent.persisted) ||
         (aBrowser.__SS_restoreState &&
          aBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE)) {
       return;
     }
     
     delete aBrowser.__SS_data;
+    delete aBrowser.__SS_tabStillLoading;
+    delete aBrowser.__SS_formDataSaved;
     this.saveStateDelayed(aWindow);
     
     // attempt to update the current URL we send in a crash report
     this._updateCrashReportURL(aWindow);
   },
 
   /**
    * Called when a browser sends the "input" notification 
    * @param aWindow
    *        Window reference
    * @param aBrowser
    *        Browser reference
    */
   onTabInput: function sss_onTabInput(aWindow, aBrowser) {
-    if (aBrowser.__SS_data)
-      delete aBrowser.__SS_data._formDataSaved;
-    
+    // deleting __SS_formDataSaved will cause us to recollect form data
+    delete aBrowser.__SS_formDataSaved;
+
     this.saveStateDelayed(aWindow, 3000);
   },
 
   /**
    * When a tab is selected, save session data
    * @param aWindow
    *        Window reference
    */
@@ -1703,17 +1724,17 @@ SessionStoreService.prototype = {
    */
   _collectTabData: function sss_collectTabData(aTab, aFullData) {
     var tabData = { entries: [] };
     var browser = aTab.linkedBrowser;
     
     if (!browser || !browser.currentURI)
       // can happen when calling this function right after .addTab()
       return tabData;
-    else if (browser.__SS_data && browser.__SS_data._tabStillLoading) {
+    else if (browser.__SS_data && browser.__SS_tabStillLoading) {
       // use the data to be restored when the tab hasn't been completely loaded
       tabData = browser.__SS_data;
       if (aTab.pinned)
         tabData.pinned = true;
       else
         delete tabData.pinned;
       tabData.hidden = aTab.hidden;
 
@@ -1738,20 +1759,21 @@ SessionStoreService.prototype = {
     if (history && browser.__SS_data &&
         browser.__SS_data.entries[history.index] &&
         browser.__SS_data.entries[history.index].url == browser.currentURI.spec &&
         history.index < this._sessionhistory_max_entries - 1 && !aFullData) {
       tabData = browser.__SS_data;
       tabData.index = history.index + 1;
     }
     else if (history && history.count > 0) {
+      browser.__SS_hostSchemeData = [];
       try {
         for (var j = 0; j < history.count; j++) {
           let entry = this._serializeHistoryEntry(history.getEntryAtIndex(j, false),
-                                                  aFullData, aTab.pinned);
+                                                  aFullData, aTab.pinned, browser.__SS_hostSchemeData);
           tabData.entries.push(entry);
         }
         // If we make it through the for loop, then we're ok and we should clear
         // any indicator of brokenness.
         delete aTab.__SS_broken_history;
       }
       catch (ex) {
         // In some cases, getEntryAtIndex will throw. This seems to be due to
@@ -1830,25 +1852,26 @@ SessionStoreService.prototype = {
    * Get an object that is a serialized representation of a History entry
    * Used for data storage
    * @param aEntry
    *        nsISHEntry instance
    * @param aFullData
    *        always return privacy sensitive data (use with care)
    * @param aIsPinned
    *        the tab is pinned and should be treated differently for privacy
+   * @param aHostSchemeData
+   *        an array of objects with host & scheme keys
    * @returns object
    */
   _serializeHistoryEntry:
-    function sss_serializeHistoryEntry(aEntry, aFullData, aIsPinned) {
+    function sss_serializeHistoryEntry(aEntry, aFullData, aIsPinned, aHostSchemeData) {
     var entry = { url: aEntry.URI.spec };
 
     try {
-      entry._host = aEntry.URI.host;
-      entry._scheme = aEntry.URI.scheme;
+      aHostSchemeData.push({ host: aEntry.URI.host, scheme: aEntry.URI.scheme });
     }
     catch (ex) {
       // We just won't attempt to get cookies for this entry.
     }
 
     if (aEntry.title && aEntry.title != entry.url) {
       entry.title = aEntry.title;
     }
@@ -1941,17 +1964,17 @@ SessionStoreService.prototype = {
     }
     
     if (aEntry.childCount > 0) {
       entry.children = [];
       for (var i = 0; i < aEntry.childCount; i++) {
         var child = aEntry.GetChildAt(i);
         if (child) {
           entry.children.push(this._serializeHistoryEntry(child, aFullData,
-                                                          aIsPinned));
+                                                          aIsPinned, aHostSchemeData));
         }
         else { // to maintain the correct frame order, insert a dummy entry 
           entry.children.push({ url: "about:blank" });
         }
         // don't try to restore framesets containing wyciwyg URLs (cf. bug 424689 and bug 450595)
         if (/^wyciwyg:\/\//.test(entry.children[i].url)) {
           delete entry.children;
           break;
@@ -2039,17 +2062,17 @@ SessionStoreService.prototype = {
    * and innerHTML content of WYSIWYG editors
    * @param aWindow
    *        Window reference
    */
   _updateTextAndScrollData: function sss_updateTextAndScrollData(aWindow) {
     var browsers = aWindow.gBrowser.browsers;
     this._windows[aWindow.__SSi].tabs.forEach(function (tabData, i) {
       if (browsers[i].__SS_data &&
-          browsers[i].__SS_data._tabStillLoading)
+          browsers[i].__SS_tabStillLoading)
         return; // ignore incompletely initialized tabs
       try {
         this._updateTextAndScrollDataForTab(aWindow, browsers[i], tabData);
       }
       catch (ex) { debug(ex); } // get as much data as possible, ignore failures (might succeed the next time)
     }, this);
   },
 
@@ -2076,19 +2099,19 @@ SessionStoreService.prototype = {
                             this._getSelectedPageStyle(aBrowser.contentWindow);
     if (selectedPageStyle)
       aTabData.pageStyle = selectedPageStyle;
     else if (aTabData.pageStyle)
       delete aTabData.pageStyle;
     
     this._updateTextAndScrollDataForFrame(aWindow, aBrowser.contentWindow,
                                           aTabData.entries[tabIndex],
-                                          !aTabData._formDataSaved, aFullData,
+                                          !aBrowser.__SS_formDataSaved, aFullData,
                                           !!aTabData.pinned);
-    aTabData._formDataSaved = true;
+    aBrowser.__SS_formDataSaved = true;
     if (aBrowser.currentURI.spec == "about:config")
       aTabData.entries[tabIndex].formdata = {
         "#textbox": aBrowser.contentDocument.getElementById("textbox").value
       };
   },
 
   /**
    * go through all subframes and store all form data, the current
@@ -2255,108 +2278,133 @@ SessionStoreService.prototype = {
    * @param aHosts
    *        the hash that will be used to store hosts eg, { hostname: true }
    * @param aCheckPrivacy
    *        should we check the privacy level for https
    * @param aIsPinned
    *        is the entry we're evaluating for a pinned tab; used only if
    *        aCheckPrivacy
    */
-  _extractHostsForCookies:
-    function sss__extractHostsForCookies(aEntry, aHosts, aCheckPrivacy, aIsPinned) {
+  _extractHostsForCookiesFromEntry:
+    function sss__extractHostsForCookiesFromEntry(aEntry, aHosts, aCheckPrivacy, aIsPinned) {
 
     let host = aEntry._host,
         scheme = aEntry._scheme;
 
     // If host & scheme aren't defined, then we are likely here in the startup
     // process via _splitCookiesFromWindow. In that case, we'll turn aEntry.url
     // into an nsIURI and get host/scheme from that. This will throw for about:
     // urls in which case we don't need to do anything.
     if (!host && !scheme) {
       try {
         let uri = this._getURIFromString(aEntry.url);
         host = uri.host;
         scheme = uri.scheme;
+        this._extractHostsForCookiesFromHostScheme(host, scheme, aHosts, aCheckPrivacy, aIsPinned);
       }
       catch(ex) { }
     }
 
-    // host and scheme may not be set (for about: urls for example), in which
-    // case testing scheme will be sufficient.
-    if (/https?/.test(scheme) && !aHosts[host] &&
-        (!aCheckPrivacy ||
-         this._checkPrivacyLevel(scheme == "https", aIsPinned))) {
-      // By setting this to true or false, we can determine when looking at
-      // the host in _updateCookies if we should check for privacy.
-      aHosts[host] = aIsPinned;
-    }
-    else if (scheme == "file") {
-      aHosts[host] = true;
-    }
-
     if (aEntry.children) {
       aEntry.children.forEach(function(entry) {
         this._extractHostsForCookies(entry, aHosts, aCheckPrivacy, aIsPinned);
       }, this);
     }
   },
 
   /**
+   * extract the base domain from a host & scheme
+   * @param aHost
+   *        the host of a uri (usually via nsIURI.host)
+   * @param aScheme
+   *        the scheme of a uri (usually via nsIURI.scheme)
+   * @param aHosts
+   *        the hash that will be used to store hosts eg, { hostname: true }
+   * @param aCheckPrivacy
+   *        should we check the privacy level for https
+   * @param aIsPinned
+   *        is the entry we're evaluating for a pinned tab; used only if
+   *        aCheckPrivacy
+   */
+  _extractHostsForCookiesFromHostScheme:
+    function sss__extractHostsForCookiesFromHostScheme(aHost, aScheme, aHosts, aCheckPrivacy, aIsPinned) {
+    // host and scheme may not be set (for about: urls for example), in which
+    // case testing scheme will be sufficient.
+    if (/https?/.test(aScheme) && !aHosts[aHost] &&
+        (!aCheckPrivacy ||
+         this._checkPrivacyLevel(aScheme == "https", aIsPinned))) {
+      // By setting this to true or false, we can determine when looking at
+      // the host in _updateCookies if we should check for privacy.
+      aHosts[aHost] = aIsPinned;
+    }
+    else if (aScheme == "file") {
+      aHosts[aHost] = true;
+    }
+  },
+
+  /**
    * store all hosts for a URL
    * @param aWindow
    *        Window reference
    */
   _updateCookieHosts: function sss_updateCookieHosts(aWindow) {
-    var hosts = this._windows[aWindow.__SSi]._hosts = {};
-
-    this._windows[aWindow.__SSi].tabs.forEach(function(aTabData) {
-      aTabData.entries.forEach(function(entry) {
-        this._extractHostsForCookies(entry, hosts, true, !!aTabData.pinned);
-      }, this);
-    }, this);
+    var hosts = this._internalWindows[aWindow.__SSi].hosts = {};
+
+    // Since _updateCookiesHosts is only ever called for open windows during a
+    // session, we can call into _extractHostsForCookiesFromHostScheme directly
+    // using data that is attached to each browser.
+    for (let i = 0; i < aWindow.gBrowser.tabs.length; i++) {
+      let tab = aWindow.gBrowser.tabs[i];
+      let hostSchemeData = tab.linkedBrowser.__SS_hostSchemeData || [];
+      for (let j = 0; j < hostSchemeData.length; j++) {
+        this._extractHostsForCookiesFromHostScheme(hostSchemeData[j].host,
+                                                   hostSchemeData[j].scheme,
+                                                   hosts, true, tab.pinned);
+      }
+    }
   },
 
   /**
    * Serialize cookie data
    * @param aWindows
-   *        array of Window references
+   *        JS object containing window data references
+   *        { id: winData, etc. }
    */
   _updateCookies: function sss_updateCookies(aWindows) {
     function addCookieToHash(aHash, aHost, aPath, aName, aCookie) {
       // lazily build up a 3-dimensional hash, with
       // aHost, aPath, and aName as keys
       if (!aHash[aHost])
         aHash[aHost] = {};
       if (!aHash[aHost][aPath])
         aHash[aHost][aPath] = {};
       aHash[aHost][aPath][aName] = aCookie;
     }
 
-    // collect the cookies per window
-    for (var i = 0; i < aWindows.length; i++)
-      aWindows[i].cookies = [];
-
     var jscookies = {};
     var _this = this;
     // MAX_EXPIRY should be 2^63-1, but JavaScript can't handle that precision
     var MAX_EXPIRY = Math.pow(2, 62);
-    aWindows.forEach(function(aWindow) {
-      if (!aWindow._hosts)
+
+    for (let [id, window] in Iterator(aWindows)) {
+      window.cookies = [];
+      let internalWindow = this._internalWindows[id];
+      if (!internalWindow.hosts)
         return;
-      for (var [host, isPinned] in Iterator(aWindow._hosts)) {
+      for (var [host, isPinned] in Iterator(internalWindow.hosts)) {
         let list;
         try {
           list = CookieSvc.getCookiesFromHost(host);
         }
         catch (ex) {
           debug("getCookiesFromHost failed. Host: " + host);
         }
         while (list && list.hasMoreElements()) {
           var cookie = list.getNext().QueryInterface(Ci.nsICookie2);
-          // aWindow._hosts will only have hosts with the right privacy rules,
+          // window._hosts will only have hosts with the right privacy rules,
           // so there is no need to do anything special with this call to
           // _checkPrivacyLevel.
           if (cookie.isSession && _this._checkPrivacyLevel(cookie.isSecure, isPinned)) {
             // use the cookie's host, path, and name as keys into a hash,
             // to make sure we serialize each cookie only once
             if (!(cookie.host in jscookies &&
                   cookie.path in jscookies[cookie.host] &&
                   cookie.name in jscookies[cookie.host][cookie.path])) {
@@ -2365,26 +2413,25 @@ SessionStoreService.prototype = {
               if (cookie.path) jscookie.path = cookie.path;
               if (cookie.name) jscookie.name = cookie.name;
               if (cookie.isSecure) jscookie.secure = true;
               if (cookie.isHttpOnly) jscookie.httponly = true;
               if (cookie.expiry < MAX_EXPIRY) jscookie.expiry = cookie.expiry;
 
               addCookieToHash(jscookies, cookie.host, cookie.path, cookie.name, jscookie);
             }
-            aWindow.cookies.push(jscookies[cookie.host][cookie.path][cookie.name]);
+            window.cookies.push(jscookies[cookie.host][cookie.path][cookie.name]);
           }
         }
       }
-    });
-
-    // don't include empty cookie sections
-    for (i = 0; i < aWindows.length; i++)
-      if (aWindows[i].cookies.length == 0)
-        delete aWindows[i].cookies;
+
+      // don't include empty cookie sections
+      if (!window.cookies.length)
+        delete window.cookies;
+    }
   },
 
   /**
    * Store window dimensions, visibility, sidebar
    * @param aWindow
    *        Window reference
    */
   _updateWindowFeatures: function sss_updateWindowFeatures(aWindow) {
@@ -2433,28 +2480,29 @@ SessionStoreService.prototype = {
         else { // always update the window features (whose change alone never triggers a save operation)
           this._updateWindowFeatures(aWindow);
         }
       });
       this._dirtyWindows = [];
     }
     
     // collect the data for all windows
-    var total = [], windows = [];
+    var total = [], windows = {}, ids = [];
     var nonPopupCount = 0;
     var ix;
     for (ix in this._windows) {
       if (this._windows[ix]._restoring) // window data is still in _statesToRestore
         continue;
       total.push(this._windows[ix]);
-      windows.push(ix);
+      ids.push(ix);
+      windows[ix] = this._windows[ix];
       if (!this._windows[ix].isPopup)
         nonPopupCount++;
     }
-    this._updateCookies(total);
+    this._updateCookies(windows);
 
     // collect the data for all windows yet to be restored
     for (ix in this._statesToRestore) {
       for each (let winData in this._statesToRestore[ix].windows) {
         total.push(winData);
         if (!winData.isPopup)
           nonPopupCount++;
       }
@@ -2495,17 +2543,17 @@ SessionStoreService.prototype = {
         return null;
 
       lastClosedWindowsCopy = [];
     }
 
     if (activeWindow) {
       this.activeWindowSSiCache = activeWindow.__SSi || "";
     }
-    ix = windows.indexOf(this.activeWindowSSiCache);
+    ix = ids.indexOf(this.activeWindowSSiCache);
     // We don't want to restore focus to a minimized window or a window which had all its
     // tabs stripped out (doesn't exist).
     if (ix != -1 && total[ix] && total[ix].sizemode == "minimized")
       ix = -1;
 
     let session = {
       state: this._loadState == STATE_RUNNING ? STATE_RUNNING_STR : STATE_STOPPED_STR,
       lastUpdate: Date.now(),
@@ -2534,20 +2582,22 @@ SessionStoreService.prototype = {
   _getWindowState: function sss_getWindowState(aWindow) {
     if (!this._isWindowLoaded(aWindow))
       return this._statesToRestore[aWindow.__SS_restoreID];
     
     if (this._loadState == STATE_RUNNING) {
       this._collectWindowData(aWindow);
     }
     
-    var total = [this._windows[aWindow.__SSi]];
-    this._updateCookies(total);
+    var winData = this._windows[aWindow.__SSi];
+    let windows = {};
+    windows[aWindow.__SSi] = winData;
+    this._updateCookies(windows);
     
-    return { windows: total };
+    return { windows: [winData] };
   },
 
   _collectWindowData: function sss_collectWindowData(aWindow) {
     if (!this._isWindowLoaded(aWindow))
       return;
     
     // update the internal state data for this window
     this._saveWindowHistory(aWindow);
@@ -2859,17 +2909,17 @@ SessionStoreService.prototype = {
       if (tabData.hidden)
         tabbrowser.hideTab(tab);
       else
         tabbrowser.showTab(tab);
 
       for (let name in tabData.attributes)
         this.xulAttributes[name] = true;
 
-      tabData._tabStillLoading = true;
+      browser.__SS_tabStillLoading = true;
 
       // keep the data around to prevent dataloss in case
       // a tab gets closed before it's been properly restored
       browser.__SS_data = tabData;
       browser.__SS_restoreState = TAB_STATE_NEEDS_RESTORE;
       tab.setAttribute("pending", "true");
 
       // Make sure that set/getTabValue will set/read the correct data by
@@ -2926,17 +2976,17 @@ SessionStoreService.prototype = {
    * @param aTabData
    *        Array of tab data
    * @param aIdMap
    *        Hash for ensuring unique frame IDs
    */
   restoreHistory:
     function sss_restoreHistory(aWindow, aTabs, aTabData, aIdMap, aDocIdentMap) {
     var _this = this;
-    while (aTabs.length > 0 && (!aTabData[0]._tabStillLoading || !aTabs[0].parentNode)) {
+    while (aTabs.length > 0 && (!aTabs[0].linkedBrowser.__SS_tabStillLoading || !aTabs[0].parentNode)) {
       aTabs.shift(); // this tab got removed before being completely restored
       aTabData.shift();
     }
     if (aTabs.length == 0) {
       // At this point we're essentially ready for consumers to read/write data
       // via the sessionstore API so we'll send the SSWindowStateReady event.
       this._setWindowStateReady(aWindow);
       return; // no more tabs to restore
@@ -3322,41 +3372,63 @@ SessionStoreService.prototype = {
         if (!hasExpectedURL(aDocument, aURL))
           return;
 
         let node = key.charAt(0) == "#" ? aDocument.getElementById(key.slice(1)) :
                                           XPathHelper.resolve(aDocument, key);
         if (!node)
           continue;
 
+        let eventType;
         let value = aData[key];
         if (typeof value == "string" && node.type != "file") {
           if (node.value == value)
             continue; // don't dispatch an input event for no change
 
           node.value = value;
-
-          let event = aDocument.createEvent("UIEvents");
-          event.initUIEvent("input", true, true, aDocument.defaultView, 0);
-          node.dispatchEvent(event);
+          eventType = "input";
         }
-        else if (typeof value == "boolean")
+        else if (typeof value == "boolean") {
+          if (node.checked == value)
+            continue; // don't dispatch a change event for no change
+
           node.checked = value;
-        else if (typeof value == "number")
+          eventType = "change";
+        }
+        else if (typeof value == "number") {
+          // We saved the value blindly since selects take more work to determine
+          // default values. So now we should check to avoid unnecessary events.
+          if (node.selectedIndex == value)
+            continue;
+
           try {
             node.selectedIndex = value;
+            eventType = "change";
           } catch (ex) { /* throws for invalid indices */ }
-        else if (value && value.fileList && value.type == "file" && node.type == "file")
+        }
+        else if (value && value.fileList && value.type == "file" && node.type == "file") {
           node.mozSetFileNameArray(value.fileList, value.fileList.length);
+          eventType = "input";
+        }
         else if (value && typeof value.indexOf == "function" && node.options) {
           Array.forEach(node.options, function(aOpt, aIx) {
             aOpt.selected = value.indexOf(aIx) > -1;
+
+            // Only fire the event here if this wasn't selected by default
+            if (!aOpt.defaultSelected)
+              eventType = "change";
           });
         }
-        // NB: dispatching "change" events might have unintended side-effects
+
+        // Fire events for this node if applicable
+        if (eventType) {
+          let event = aDocument.createEvent("UIEvents");
+          event.initUIEvent(eventType, true, true, aDocument.defaultView, 0);
+          node.dispatchEvent(event);
+        }
       }
     }
 
     let selectedPageStyle = aBrowser.__SS_restore_pageStyle;
     function restoreTextDataAndScrolling(aContent, aData, aPrefix) {
       if (aData.formdata)
         restoreFormData(aContent.document, aData.formdata, aData.url);
       if (aData.innerHTML) {
@@ -3558,16 +3630,17 @@ SessionStoreService.prototype = {
 
 #ifndef XP_MACOSX
     // We want to restore closed windows that are marked with _shouldRestore.
     // We're doing this here because we want to control this only when saving
     // the file.
     while (oState._closedWindows.length) {
       let i = oState._closedWindows.length - 1;
       if (oState._closedWindows[i]._shouldRestore) {
+        delete oState._closedWindows[i]._shouldRestore;
         oState.windows.unshift(oState._closedWindows.pop());
       }
       else {
         // We only need to go until we hit !needsRestore since we're going in reverse
         break;
       }
     }
 #endif
@@ -4026,17 +4099,17 @@ SessionStoreService.prototype = {
     function sss__splitCookiesFromWindow(aWinState, aTargetWinState) {
     if (!aWinState.cookies || !aWinState.cookies.length)
       return;
 
     // Get the hosts for history entries in aTargetWinState
     let cookieHosts = {};
     aTargetWinState.tabs.forEach(function(tab) {
       tab.entries.forEach(function(entry) {
-        this._extractHostsForCookies(entry, cookieHosts, false)
+        this._extractHostsForCookiesFromEntry(entry, cookieHosts, false);
       }, this);
     }, this);
 
     // By creating a regex we reduce overhead and there is only one loop pass
     // through either array (cookieHosts and aWinState.cookies).
     let hosts = Object.keys(cookieHosts).join("|").replace("\\.", "\\.", "g");
     // If we don't actually have any hosts, then we don't want to do anything.
     if (!hosts.length)
@@ -4057,28 +4130,17 @@ SessionStoreService.prototype = {
    * (see http://www.json.org/ for more information).
    *
    * The inverse operation consists of JSON.parse(JSON_string).
    *
    * @param aJSObject is the object to be converted
    * @returns the object's JSON representation
    */
   _toJSONString: function sss_toJSONString(aJSObject) {
-    // We never want to save __lastSessionWindowID across sessions, but we do
-    // want it exported to consumers when running (eg. Private Browsing).
-    let internalKeys = INTERNAL_KEYS;
-    if (this._loadState == STATE_QUITTING) {
-      internalKeys = internalKeys.slice();
-      internalKeys.push("__lastSessionWindowID");
-    }
-    function exclude(key, value) {
-      // returning undefined results in the exclusion of that key
-      return internalKeys.indexOf(key) == -1 ? value : undefined;
-    }
-    return JSON.stringify(aJSObject, exclude);
+    return JSON.stringify(aJSObject);
   },
 
   _sendRestoreCompletedNotifications: function sss_sendRestoreCompletedNotifications() {
     // not all windows restored, yet
     if (this._restoreCount > 1) {
       this._restoreCount--;
       return;
     }
--- a/browser/components/sessionstore/test/browser/Makefile.in
+++ b/browser/components/sessionstore/test/browser/Makefile.in
@@ -45,16 +45,18 @@ relativesrcdir  = browser/components/ses
 include $(DEPTH)/config/autoconf.mk
 include $(topsrcdir)/config/rules.mk
 
 # browser_506482.js is disabled because of frequent failures (bug 538672)
 # browser_526613.js is disabled because of frequent failures (bug 534489)
 
 _BROWSER_TEST_FILES = \
 	head.js \
+	browser_form_restore_events.js \
+	browser_form_restore_events_sample.html \
 	browser_248970_a.js \
 	browser_248970_b.js \
 	browser_248970_b_sample.html \
 	browser_339445.js \
 	browser_339445_sample.html \
 	browser_345898.js \
 	browser_346337.js \
 	browser_346337_sample.html \
@@ -93,18 +95,16 @@ include $(topsrcdir)/config/rules.mk
 	browser_464620_a.html \
 	browser_464620_b.js \
 	browser_464620_b.html \
 	browser_464620_xd.html \
 	browser_465215.js \
 	browser_465223.js \
 	browser_466937.js \
 	browser_466937_sample.html \
-	browser_476161.js \
-	browser_476161_sample.html \
 	browser_477657.js \
 	browser_480148.js \
 	browser_480893.js \
 	browser_483330.js \
 	browser_485482.js \
 	browser_485482_sample.html \
 	browser_485563.js \
 	browser_490040.js \
--- a/browser/components/sessionstore/test/browser/browser_579868.js
+++ b/browser/components/sessionstore/test/browser/browser_579868.js
@@ -45,17 +45,17 @@ function test() {
 
     // Tell the session storer that the tab is pinned
     let newTabState = '{"entries":[{"url":"about:robots"}],"pinned":true,"userTypedValue":"Hello World!"}';
     ss.setTabState(tab1, newTabState);
 
     // Undo pinning
     gBrowser.unpinTab(tab1);
 
-    is(tab1.linkedBrowser.__SS_data._tabStillLoading, true, 
+    is(tab1.linkedBrowser.__SS_tabStillLoading, true,
        "_tabStillLoading should be true.");
 
     // Close and restore tab
     gBrowser.removeTab(tab1);
     let savedState = JSON.parse(ss.getClosedTabData(window))[0].state;
     isnot(savedState.pinned, true, "Pinned should not be true");
     tab1 = ss.undoCloseTab(window, 0);
 
rename from browser/components/sessionstore/test/browser/browser_476161.js
rename to browser/components/sessionstore/test/browser/browser_form_restore_events.js
--- a/browser/components/sessionstore/test/browser/browser_476161.js
+++ b/browser/components/sessionstore/test/browser/browser_form_restore_events.js
@@ -30,39 +30,70 @@
  * decision by deleting the provisions above and replace them with the notice
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 function test() {
-  /** Test for Bug 476161 **/
-  
+  /** Originally a test for Bug 476161, but then expanded to include all input types in bug 640136 **/
+
   waitForExplicitFinish();
-  
+
+  let file = Components.classes["@mozilla.org/file/directory_service;1"]
+             .getService(Components.interfaces.nsIProperties)
+             .get("TmpD", Components.interfaces.nsIFile);
+
   let testURL = "http://mochi.test:8888/browser/" +
-    "browser/components/sessionstore/test/browser/browser_476161_sample.html";
+    "browser/components/sessionstore/test/browser/browser_form_restore_events_sample.html";
   let tab = gBrowser.addTab(testURL);
   tab.linkedBrowser.addEventListener("load", function(aEvent) {
     tab.linkedBrowser.removeEventListener("load", arguments.callee, true);
     let doc = tab.linkedBrowser.contentDocument;
-    
-    doc.getElementById("modify1").value += Math.random();
-    doc.getElementById("modify2").value += " " + Date.now();
-    
+
+    // text fields
+    doc.getElementById("modify01").value += Math.random();
+    doc.getElementById("modify02").value += " " + Date.now();
+
+    // textareas
+    doc.getElementById("modify03").value += Math.random();
+    doc.getElementById("modify04").value += " " + Date.now();
+
+    // file
+    doc.getElementById("modify05").value = file.path;
+
+    // select
+    doc.getElementById("modify06").selectedIndex = 1;
+    var multipleChange = doc.getElementById("modify07");
+    Array.forEach(multipleChange.options, function(option) option.selected = true);
+
+    // checkbox
+    doc.getElementById("modify08").checked = true;
+    doc.getElementById("modify09").checked = false;
+
+    // radio
+    // select one then another in the same group - only last one should get event on restore
+    doc.getElementById("modify10").checked = true;
+    doc.getElementById("modify11").checked = true;
+
+
     let tab2 = gBrowser.duplicateTab(tab);
     tab2.linkedBrowser.addEventListener("load", function(aEvent) {
       tab2.linkedBrowser.removeEventListener("load", arguments.callee, true);
       let doc = tab2.linkedBrowser.contentDocument;
-      let changed = doc.getElementById("changed").textContent.trim().split();
-      
-      is(changed.sort().join(" "), "modify1 modify2",
-         "input events were only dispatched for modified text fields");
-      
+      let inputFired = doc.getElementById("inputFired").textContent.trim().split();
+      let changeFired = doc.getElementById("changeFired").textContent.trim().split();
+
+      is(inputFired.sort().join(" "), "modify01 modify02 modify03 modify04 modify05",
+         "input events were only dispatched for modified input, textarea fields");
+
+      is(changeFired.sort().join(" "), "modify06 modify07 modify08 modify09 modify11",
+         "change events were only dispatched for modified select, checkbox, radio fields");
+
       // clean up
       gBrowser.removeTab(tab2);
       gBrowser.removeTab(tab);
-      
+
       finish();
     }, true);
   }, true);
 }
rename from browser/components/sessionstore/test/browser/browser_476161_sample.html
rename to browser/components/sessionstore/test/browser/browser_form_restore_events_sample.html
--- a/browser/components/sessionstore/test/browser/browser_476161_sample.html
+++ b/browser/components/sessionstore/test/browser/browser_form_restore_events_sample.html
@@ -1,24 +1,98 @@
 <!DOCTYPE html>
-<title>Test for bug 476161</title>
+<title>Test for form restore events (originally bug 476161)</title>
 
 <script>
 
 document.addEventListener("input", function(aEvent) {
   var inputEl = aEvent.originalTarget;
-  var changedEl = document.getElementById("changed");
-  
+  var changedEl = document.getElementById("inputFired");
+  changedEl.textContent += " " + inputEl.id;
+}, false);
+
+document.addEventListener("change", function(aEvent) {
+  var inputEl = aEvent.originalTarget;
+  var changedEl = document.getElementById("changeFired");
   changedEl.textContent += " " + inputEl.id;
 }, false);
 
 </script>
 
+<!-- input events -->
 <h3>Text fields with changed text</h3>
 <input type="text" id="modify1">
 <input type="text" id="modify2" value="preset value">
+<input type="text" id="modify01">
+<input type="text" id="modify02" value="preset value">
 
 <h3>Text fields with unchanged text</h3>
 <input type="text" id="unchanged1">
 <input type="text" id="unchanged2" value="preset value">
+<input type="text" id="unchanged01">
+<input type="text" id="unchanged02" value="preset value">
+
+<h3>Textarea with changed text</h3>
+<textarea id="modify03"></textarea>
+<textarea id="modify04">preset value</textarea>
+
+<h3>Textarea with unchanged text</h3>
+<textarea id="unchanged03"></textarea>
+<textarea id="unchanged04">preset value</textarea>
+
+<h3>file field with changed value</h3>
+<input type="file" id="modify05">
+
+<h3>file field with unchanged value</h3>
+<input type="file" id="unchanged05">
+
+<!-- change events -->
+
+<h3>Select menu with changed selection</h3>
+<select id="modify06">
+  <option value="one">one</option>
+  <option value="two">two</option>
+  <option value="three">three</option>
+</select>
+
+<h3>Select menu with unchanged selection (change event still fires)</h3>
+<select id="unchanged06">
+  <option value="one">one</option>
+  <option value="two" selected>two</option>
+  <option value="three">three</option>
+</select>
+
+<h3>Multiple Select menu with changed selection</h3>
+<select id="modify07" multiple>
+  <option value="one">one</option>
+  <option value="two" selected>two</option>
+  <option value="three">three</option>
+</select>
+
+<h3>Select menu with unchanged selection</h3>
+<select id="unchanged07" multiple>
+  <option value="one">one</option>
+  <option value="two" selected>two</option>
+  <option value="three" selected>three</option>
+</select>
+
+<h3>checkbox with changed value</h3>
+<input type="checkbox" id="modify08">
+<input type="checkbox" id="modify09" checked>
+
+<h3>checkbox with unchanged value</h3>
+<input type="checkbox" id="unchanged08">
+<input type="checkbox" id="unchanged09" checked>
+
+<h3>radio with changed value</h3>
+<input type="radio" id="modify10"  name="group">Radio 1</input>
+<input type="radio" id="modify11"  name="group">Radio 2</input>
+<input type="radio" id="modify12" name="group" checked>Radio 3</input>
+
+<h3>radio with unchanged value</h3>
+<input type="radio" id="unchanged10"  name="group2">Radio 4</input>
+<input type="radio" id="unchanged11"  name="group2">Radio 5</input>
+<input type="radio" id="unchanged12" name="group2" checked>Radio 6</input>
 
 <h3>Changed field IDs</h3>
 <div id="changed"></div>
+<div id="inputFired"></div>
+<div id="changeFired"></div>
--- a/content/events/src/nsEventStateManager.cpp
+++ b/content/events/src/nsEventStateManager.cpp
@@ -2091,16 +2091,17 @@ nsEventStateManager::GenerateDragGesture
 
       // Stop tracking the drag gesture now. This should stop us from
       // reentering GenerateDragGesture inside DOM event processing.
       StopTrackingDragGesture();
 
       if (!targetContent)
         return;
 
+      sLastDragOverFrame = nsnull;
       nsCOMPtr<nsIWidget> widget = mCurrentTarget->GetNearestWidget();
 
       // get the widget from the target frame
       nsDragEvent startEvent(NS_IS_TRUSTED_EVENT(aEvent), NS_DRAGDROP_START, widget);
       FillInEventFromGestureDown(&startEvent);
 
       nsDragEvent gestureEvent(NS_IS_TRUSTED_EVENT(aEvent), NS_DRAGDROP_GESTURE, widget);
       FillInEventFromGestureDown(&gestureEvent);
@@ -3417,16 +3418,17 @@ nsEventStateManager::PostHandleEvent(nsP
 
         nsEventStatus status = nsEventStatus_eIgnore;
         nsCOMPtr<nsIPresShell> presShell = mPresContext->GetPresShell();
         if (presShell) {
           presShell->HandleEventWithTarget(&event, mCurrentTarget,
                                            targetContent, &status);
         }
       }
+      sLastDragOverFrame = nsnull;
       ClearGlobalActiveContent(this);
       break;
     }
   case NS_DRAGDROP_EXIT:
      // make sure to fire the enter and exit_synth events after the
      // NS_DRAGDROP_EXIT event, otherwise we'll clean up too early
     GenerateDragDropEnterExit(presContext, (nsGUIEvent*)aEvent);
     break;